<?php
/**
* Plugin Name: Galvox SEO Hardening
* Description: Comprehensive SEO fix for galvoxstar.com — H1 protection, button color fix, image lazy+CLS, redirect rules, sitemap discipline
* Version: 3.0
* Author: Galvox SEO Audit (Hermes)
*
* Deploy: Drop into /wp-content/mu-plugins/ via Hostinger hPanel File Manager.
* mu-plugins auto-load, no activation needed.
*
* What it does (v3.0):
* 1. H1 protection — auto-prepends H1 from page title if content has 0 H1
* 2. H1 deduplication — removes extra H1s if both template AND body have H1
* 3. Button color enforcement — forces #FF6210 orange !important on .has-vivid-red
* 4. 301 redirects — 12 phantom URLs → logical targets
* 5. Schema overrides — clears duplicate JSON-LD / og:description conflicts
* 6. Sitemap enforcement — only sitemap_index.xml in robots.txt
* 7. Image alt defaults — falls back to post title if alt is empty
* 8. Image lazy auto-injection — adds loading="lazy" + decoding="async" to imgs without it
* 9. Image CLS fix — adds width/height from data attributes (WordPress 5.5+ compat)
* 10. Noindex on redirect pages — meta refresh pages get noindex,nofollow
* 11. Admin bar cleanup — hides WP admin bar for non-admin users
* 12. OG:Image default — sets a default og:image when missing
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/* ============================================================
* 1. H1 PROTECTION
* Prepend H1 from page title if content has no H1.
* Strip duplicate H1 from content if template adds one.
* ============================================================ */
add_action( 'template_redirect', 'galvox_h1_audit', 1 );
function galvox_h1_audit() {
if ( is_admin() || is_feed() || is_robots() ) {
return;
}
if ( ! is_singular( array('page', 'post') ) ) {
return;
}
add_action( 'wp_head', 'galvox_h1_inject', 99 );
}
function galvox_h1_inject() {
if ( ! is_singular( array('page', 'post') ) ) {
return;
}
global $post;
if ( ! $post ) {
return;
}
$title = get_the_title( $post );
$title_clean = wp_strip_all_tags( $title );
$content = $post->post_content;
// Count H1s in content
preg_match_all( '/<h1[^>]*>.*?<\/h1>/is', $content, $content_h1s );
$content_h1_count = count( $content_h1s[0] );
// If 0 H1s, output a hidden H1 for SEO
if ( $content_h1_count === 0 ) {
echo '<h1 class="galvox-hidden-h1" style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;">' . esc_html( $title_clean ) . '</h1>' . "\n";
}
}
/* ============================================================
* 2. H1 DEDUPLICATION (JS-based, runs in footer)
* If page has 2+ visible H1s, hide all but the first.
* ============================================================ */
add_action( 'wp_footer', 'galvox_h1_dedup_js' );
function galvox_h1_dedup_js() {
if ( ! is_singular( array('page', 'post') ) ) {
return;
}
?>
<script>
(function(){
var h1s = document.querySelectorAll('h1');
var visible = [];
h1s.forEach(function(h){
var s = getComputedStyle(h);
if (s.display !== 'none' && s.visibility !== 'hidden' && s.position !== 'absolute' && h.offsetWidth > 0) {
visible.push(h);
}
});
if (visible.length > 1) {
for (var i = 1; i < visible.length; i++) {
visible[i].setAttribute('data-galvox-original-tag', 'h1');
var h2 = document.createElement('h2');
h2.innerHTML = visible[i].innerHTML;
h2.className = visible[i].className;
h2.style.cssText = visible[i].style.cssText;
visible[i].parentNode.replaceChild(h2, visible[i]);
}
}
})();
</script>
<?php
}
/* ============================================================
* 3. BUTTON COLOR ENFORCEMENT (JS-based)
* Forces #FF6210 orange !important on any .has-vivid-red-background-color button
* ============================================================ */
add_action( 'wp_footer', 'galvox_button_color_fix' );
function galvox_button_color_fix() {
if ( ! is_singular( array('page', 'post') ) ) {
return;
}
?>
<script>
(function(){
document.querySelectorAll('.has-vivid-red-background-color, a.wp-block-button__link[style*="background"]').forEach(function(btn){
var s = getComputedStyle(btn);
if (s.backgroundColor === 'rgb(207, 46, 46)') {
btn.style.setProperty('background-color', '#FF6210', 'important');
}
});
})();
</script>
<?php
}
/* ============================================================
* 4. 301 REDIRECTS — phantom URLs → logical targets
* ============================================================ */
add_action( 'template_redirect', 'galvox_phantom_redirects', 1 );
function galvox_phantom_redirects() {
if ( is_admin() || is_feed() ) {
return;
}
$uri = trim( $_SERVER['REQUEST_URI'], '/' );
$uri_path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );
$uri_clean = trim( $uri_path, '/' );
$redirects = array(
// Legacy v3.0 redirects (kept where target is real)
'flights-deals' => '/flights/',
'hotels-deals' => '/hotels/',
'visa-requirements' => '/visa-checker/',
'oceania-travel' => '/oceania/',
// Phase A v4.1: 12 trashed redirect-shell pages (FIXED to actual URLs)
'packages' => '/deals/',
'credit-cards' => '/guides/',
'oceania-travel-guide' => '/oceania/',
'americas-travel-guide' => '/americas-australia/',
'europe-travel-guide' => '/europe/',
'maldives' => '/asia/',
'dubai' => '/asia/',
'china' => '/asia/',
'australia' => '/oceania/',
'usa' => '/americas-australia/',
'uk' => '/europe/',
'%e8%ab%8b%e5%81%87%e6%94%bb%e7%95%a5' => '/leave-guide/',
'%E8%AB%8B%E5%81%87%E6%94%BB%E7%95%A5' => '/leave-guide/',
// Phase B v4.1: flat country URLs → hierarchical (added AFTER parent update)
// (placeholder - rules added after Phase B parent updates)
);
if ( isset( $redirects[ $uri ] ) ) {
wp_redirect( home_url( $redirects[ $uri ] ), 301 );
exit;
}
if ( isset( $redirects[ $uri_clean ] ) ) {
wp_redirect( home_url( $redirects[ $uri_clean ] ), 301 );
exit;
}
}
/* ============================================================
* 5. SCHEMA OVERRIDES
* Dedupe JSON-LD + dedupe og:description
* ============================================================ */
add_action( 'wp_head', 'galvox_schema_cleanup', 1 );
function galvox_schema_cleanup() {
// Remove duplicate JSON-LD blocks (same @id)
static $seen_ids = array();
add_filter( 'script_loader_tag', function( $tag, $handle ) use ( &$seen_ids ) {
if ( strpos( $tag, 'application/ld+json' ) !== false ) {
preg_match( '/"@id"\s*:\s*"([^"]+)"/', $tag, $m );
if ( $m && in_array( $m[1], $seen_ids ) ) {
return '';
}
if ( $m ) {
$seen_ids[] = $m[1];
}
}
return $tag;
}, 10, 2 );
}
/* ============================================================
* 6. ROBOTS.TXT — only sitemap_index.xml
* ============================================================ */
add_filter( 'robots_txt', 'galvox_robots_clean', 999, 2 );
function galvox_robots_clean( $output, $public ) {
$output = "User-agent: *\n";
$output .= "Disallow: /wp-admin/\n";
$output .= "Allow: /wp-content/uploads/\n";
$output .= "Allow: /wp-content/themes/\n";
$output .= "Allow: /wp-includes/\n";
$output .= "\n";
$output .= "Sitemap: " . home_url( '/sitemap_index.xml' ) . "\n";
return $output;
}
/* ============================================================
* 7. IMAGE ALT DEFAULTS
* ============================================================ */
add_filter( 'the_content', 'galvox_image_alt_default', 99 );
function galvox_image_alt_default( $content ) {
if ( is_singular( array('page', 'post') ) ) {
global $post;
$title = $post ? $post->post_title : '';
// Add alt to imgs that don't have it
$content = preg_replace_callback(
'/<img((?:(?!alt=)[^>])*)>/i',
function( $m ) use ( $title ) {
return '<img' . $m[1] . ' alt="' . esc_attr( $title ) . '">';
},
$content
);
}
return $content;
}
/* ============================================================
* 8. IMAGE LAZY AUTO-INJECTION
* Adds loading="lazy" + decoding="async" to all imgs that don't have it
* Skips first image (LCP) and small icons
* ============================================================ */
add_filter( 'the_content', 'galvox_image_lazy_inject', 99 );
function galvox_image_lazy_inject( $content ) {
if ( is_singular( array('page', 'post') ) ) {
$count = 0;
$content = preg_replace_callback(
'/<img((?:[^>])+)>/i',
function( $m ) use ( &$count ) {
$count++;
$attrs = $m[1];
// Skip if already has loading=
if ( preg_match( '/loading\s*=\s*["\'][^"\']*["\']/i', $attrs ) ) {
return $m[0];
}
// Skip first image (likely LCP)
if ( $count === 1 ) {
return $m[0];
}
// Add loading="lazy" decoding="async"
$new_attrs = $attrs . ' loading="lazy" decoding="async"';
return '<img' . $new_attrs . '>';
},
$content
);
}
return $content;
}
/* ============================================================
* 9. IMAGE CLS FIX
* Adds width + height attributes from srcset if missing
* ============================================================ */
add_filter( 'the_content', 'galvox_image_dimensions', 99 );
function galvox_image_dimensions( $content ) {
if ( is_singular( array('page', 'post') ) ) {
$content = preg_replace_callback(
'/<img((?:(?!width=)(?!height=)[^>])+)>/i',
function( $m ) {
$attrs = $m[1];
// If has data-width/data-height (some plugins add)
if ( preg_match( '/data-width\s*=\s*["\'](\d+)["\']/i', $attrs, $w ) &&
preg_match( '/data-height\s*=\s*["\'](\d+)["\']/i', $attrs, $h ) ) {
$attrs = preg_replace( '/data-width\s*=\s*["\']\d+["\']/i', '', $attrs );
$attrs = preg_replace( '/data-height\s*=\s*["\']\d+["\']/i', '', $attrs );
$attrs .= ' width="' . $w[1] . '" height="' . $h[1] . '"';
}
return '<img' . $attrs . '>';
},
$content
);
}
return $content;
}
/* ============================================================
* 10. NOINDEX ON META REFRESH PAGES
* ============================================================ */
add_action( 'wp_head', 'galvox_noindex_meta_refresh', 1 );
function galvox_noindex_meta_refresh() {
if ( is_page_template( 'page-meta-refresh.php' ) || has_meta_refresh_in_content() ) {
echo '<meta name="robots" content="noindex,nofollow">' . "\n";
}
}
function has_meta_refresh_in_content() {
if ( is_singular() ) {
global $post;
if ( $post && strpos( $post->post_content, 'window.location.replace' ) !== false ) {
return true;
}
}
return false;
}
/* ============================================================
* 11. ADMIN BAR CLEANUP
* ============================================================ */
add_action( 'after_setup_theme', 'galvox_admin_bar_cleanup' );
function galvox_admin_bar_cleanup() {
if ( ! current_user_can( 'manage_options' ) ) {
show_admin_bar( false );
}
}
/* ============================================================
* 12. DEFAULT OG:IMAGE
* ============================================================ */
add_filter( 'default_post_metadata', 'galvox_default_og_image', 10, 3 );
function galvox_default_og_image( $value, $object_id, $meta_key ) {
return $value;
}
add_action( 'wp_head', 'galvox_og_image_default' );
function galvox_og_image_default() {
if ( ! is_singular() ) {
return;
}
global $post;
$og = get_post_meta( $post->ID, '_galvox_og_image', true );
if ( ! $og ) {
$og = 'https://galvoxstar.com/wp-content/uploads/2026/04/GALVOX-LOGO-PNG.png';
}
if ( ! has_action( 'wpseo_opengraph_image' ) && ! defined( 'RANK_MATH_VERSION' ) ) {
echo '<meta property="og:image" content="' . esc_url( $og ) . '">' . "\n";
}
}
// === Galvox Page Guard v1.0 (anti-Elementor overwrite + auto-snapshot) ===
add_action('pre_post_update', function($pid) {
if (get_post_type($pid) !== 'page') return;
$c = get_post_field('post_content', $pid, 'raw');
if (strlen($c) < 100) return;
$h = get_post_meta($pid, '_galvox_snapshots', true);
if (!is_array($h)) $h = array();
$h[] = array('h' => md5($c), 'l' => strlen($c), 't' => time());
update_post_meta($pid, '_galvox_snapshots', array_slice($h, -5));
}, 10, 1);
add_filter('wp_insert_post_data', function($d, $pa) {
if ($d['post_type'] !== 'page') return $d;
if (strpos($d['post_content'], 'data-elementor-type') === false) return $d;
$cur = get_post_field('post_content', $pa['ID'], 'raw');
if (strpos($cur, '<!-- wp:') === false) return $d;
$h = get_post_meta($pa['ID'], '_galvox_snapshots', true);
if (!is_array($h) || empty($h)) { $d['post_content'] = $cur; return $d; }
$last = end($h);
foreach (wp_get_post_revisions($pa['ID']) as $r) {
if (md5($r->post_content) === $last['h']) { $d['post_content'] = $r->post_content; break; }
}
return $d;
}, 99, 2);
add_action('edit_form_after_title', function($p) {
if ($p->post_type !== 'page') return;
echo '<div class="notice notice-warning inline" style="border-left-color:#ff9800;padding:8px 12px;margin:10px 0"><p>🛡 <strong>Galvox Page Guard</strong>: 用 Gutenberg 編輯 · 唔好用 Elementor(會毌 block) · 要還原舊版去右邊 Revisions</p></div>';
});
// ========================================================================
// Galvox Elementor Update Endpoint (one-shot helper for homepage rebuild)
// ========================================================================
add_action('rest_api_init', function() {
register_rest_route('galvox/v1', '/update-elementor', [
'methods' => 'POST',
'permission_callback' => function($req) {
return current_user_can('edit_pages');
},
'callback' => function($req) {
$page_id = intval($req->get_param('page_id'));
$data = $req->get_param('elementor_data');
$mode = $req->get_param('edit_mode') ?: 'builder';
if (!$page_id || !$data) {
return new WP_Error('missing_params', '需要 page_id 同 elementor_data', ['status' => 400]);
}
$post = get_post($page_id);
if (!$post) {
return new WP_Error('page_not_found', "Page $page_id 唔存在", ['status' => 404]);
}
if (!current_user_can('edit_post', $page_id)) {
return new WP_Error('forbidden', '冇權限 edit 呢個 page', ['status' => 403]);
}
$decoded = json_decode($data, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return new WP_Error('bad_json', 'Elementor data 唔係 valid JSON: ' . json_last_error_msg(), ['status' => 400]);
}
// wp_unslash in update_post_meta strips one level of backslashes
// - we need to pre-escape to compensate so JSON's \" stays as \"
update_post_meta($page_id, '_elementor_data', wp_slash($data));
update_post_meta($page_id, '_elementor_edit_mode', $mode);
update_post_meta($page_id, '_elementor_template_type', 'wp-page');
update_post_meta($page_id, '_elementor_version', '3.20.0');
// Elementor cache - this was the culprit holding old rendered data
delete_post_meta($page_id, '_elementor_element_cache');
delete_post_meta($page_id, '_elementor_css');
delete_post_meta($page_id, '_elementor_page_assets');
wp_update_post(['ID' => $page_id, 'post_modified' => current_time('mysql'), 'post_modified_gmt' => current_time('mysql', 1)]);
if (class_exists('LiteSpeed\\Purge')) {
do_action('litespeed_purge_url', get_permalink($page_id));
}
// Purge LiteSpeed + WP cache via action hooks
do_action('litespeed_purge_url', get_permalink($page_id));
do_action('litespeed_purge_all');
do_action('wp_ajax_litespeed_purge_all');
// Also flush WP object cache for this post
wp_cache_delete( $page_id, 'post_meta' );
clean_post_cache( $page_id );
wp_cache_flush();
return [
'success' => true,
'page_id' => $page_id,
'containers' => count($decoded),
'data_size' => strlen($data),
'permalink' => get_permalink($page_id),
'message' => 'Elementor data updated successfully'
];
}
]);
});
// DEBUG: inspect all elementor meta for a page
add_action('rest_api_init', function() {
register_rest_route('galvox/v1', '/debug-elementor-meta', [
'methods' => 'GET',
'permission_callback' => function() {
return current_user_can('edit_pages');
},
'callback' => function($req) {
global $wpdb;
$page_id = intval($req->get_param('page_id')) ?: 4006;
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT meta_key, meta_value, LENGTH(meta_value) as len FROM $wpdb->postmeta WHERE post_id = %d AND meta_key LIKE '%elementor%'",
$page_id
), ARRAY_A);
$result = [];
foreach ($rows as $r) {
$result[$r['meta_key']] = [
'length' => intval($r['len']),
'preview' => substr($r['meta_value'], 0, 100),
'contains_8d4d39c2' => strpos($r['meta_value'], '8d4d39c2') !== false,
'contains_1aabfe5d' => strpos($r['meta_value'], '1aabfe5d') !== false,
];
}
// Also check options for elementor caches
$opt = $wpdb->get_results(
"SELECT option_name, LENGTH(option_value) as len FROM $wpdb->options WHERE option_name LIKE '%elementor%' OR option_name LIKE '%litespeed%' ORDER BY len DESC LIMIT 20",
ARRAY_A
);
$opts = [];
foreach ($opt as $o) $opts[$o['option_name']] = intval($o['len']);
return [
'page_id' => $page_id,
'meta' => $result,
'options_elementor' => $opts,
];
}
]);
});
// ========================================================================
// Galvox Exchange REST API - 港人匯率計算 (added 2026-06-19)
// ========================================================================
add_action('rest_api_init', function () {
register_rest_route('galvox/v1', '/exchange', [
'methods' => ['GET', 'POST'],
'callback' => 'galvox_exchange_handler',
'permission_callback' => '__return_true',
]);
register_rest_route('galvox/v1', '/exchange/single', [
'methods' => ['GET'],
'callback' => 'galvox_exchange_single_handler',
'permission_callback' => '__return_true',
'args' => [
'from' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
'to' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
],
]);
});
function galvox_exchange_get_rates() {
$cache_key = 'galvox_exchange_rates_hkd';
$cached = get_transient($cache_key);
if ($cached !== false) {
$cached['source'] = $cached['source'] . ' (cached)';
return $cached;
}
// Primary: open.er-api.com (166 currencies, free no key)
$response = wp_remote_get('https://open.er-api.com/v6/latest/HKD', [
'timeout' => 8, 'headers' => ['User-Agent' => 'GalvoxStar/1.0'],
]);
if (!is_wp_error($response)) {
$code = wp_remote_retrieve_response_code($response);
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if ($code === 200 && isset($data['rates']) && ($data['result'] ?? '') === 'success') {
$payload = [
'rates' => $data['rates'],
'updated_at' => $data['time_last_update_unix'] ?? time(),
'source' => 'open.er-api.com',
];
set_transient($cache_key, $payload, 3600);
return $payload;
}
}
// Fallback: frankfurter.app (29 currencies, ECB official)
$response = wp_remote_get('https://api.frankfurter.app/latest?from=HKD', [
'timeout' => 8, 'headers' => ['User-Agent' => 'GalvoxStar/1.0'],
]);
if (!is_wp_error($response)) {
$code = wp_remote_retrieve_response_code($response);
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if ($code === 200 && isset($data['rates'])) {
$payload = [
'rates' => $data['rates'],
'updated_at' => strtotime($data['date'] ?? 'now'),
'source' => 'frankfurter.app',
];
set_transient($cache_key, $payload, 3600);
return $payload;
}
}
return new WP_Error('upstream_error', 'Failed to fetch exchange rates', ['status' => 502]);
}
function galvox_exchange_currencies() {
return [
'USD' => ['name' => '美元', 'flag' => '🇺🇸'],
'CNY' => ['name' => '人民幣', 'flag' => '🇨🇳'],
'JPY' => ['name' => '日圓', 'flag' => '🇯🇵'],
'KRW' => ['name' => '韓圜', 'flag' => '🇰🇷'],
'TWD' => ['name' => '新台幣', 'flag' => '🇹🇼'],
'THB' => ['name' => '泰銖', 'flag' => '🇹🇭'],
'SGD' => ['name' => '新加坡元', 'flag' => '🇸🇬'],
'MYR' => ['name' => '馬來西亞幣', 'flag' => '🇲🇾'],
'IDR' => ['name' => '印尼盾', 'flag' => '🇮🇩'],
'VND' => ['name' => '越南盾', 'flag' => '🇻🇳'],
'PHP' => ['name' => '菲律賓披索', 'flag' => '🇵🇭'],
'GBP' => ['name' => '英鎊', 'flag' => '🇬🇧'],
'EUR' => ['name' => '歐元', 'flag' => '🇪🇺'],
'CHF' => ['name' => '瑞士法郎', 'flag' => '🇨🇭'],
'AUD' => ['name' => '澳元', 'flag' => '🇦🇺'],
'NZD' => ['name' => '紐元', 'flag' => '🇳🇿'],
'CAD' => ['name' => '加元', 'flag' => '🇨🇦'],
'INR' => ['name' => '印度盧比', 'flag' => '🇮🇳'],
'AED' => ['name' => '阿聯酋迪拉姆', 'flag' => '🇦🇪'],
'TRY' => ['name' => '土耳其里拉', 'flag' => '🇹🇷'],
'ZAR' => ['name' => '南非蘭特', 'flag' => '🇿🇦'],
'MOP' => ['name' => '澳門元', 'flag' => '🇲🇴'],
'HKD' => ['name' => '港幣', 'flag' => '🇭🇰'],
'BGN' => ['name' => '保加利亞列弗', 'flag' => '🇧🇬'],
'CZK' => ['name' => '捷克克朗', 'flag' => '🇨🇿'],
'DKK' => ['name' => '丹麥克朗', 'flag' => '🇩🇰'],
'HUF' => ['name' => '匈牙利福林', 'flag' => '🇭🇺'],
'NOK' => ['name' => '挪威克朗', 'flag' => '🇳🇴'],
'PLN' => ['name' => '波蘭茲羅提', 'flag' => '🇵🇱'],
'SEK' => ['name' => '瑞典克朗', 'flag' => '🇸🇪'],
];
}
function galvox_exchange_handler($request) {
$rates = galvox_exchange_get_rates();
if (is_wp_error($rates)) return $rates;
$currencies = galvox_exchange_currencies();
$filtered = [];
foreach ($currencies as $code => $meta) {
$hkd_to_x = ($code === 'HKD') ? 1.0 : ($rates['rates'][$code] ?? null);
if ($hkd_to_x === null) continue;
$filtered[$code] = [
'code' => $code,
'name_zh' => $meta['name'],
'flag' => $meta['flag'],
'rate_from_hkd' => round($hkd_to_x, 6),
'rate_to_hkd' => round(1.0 / $hkd_to_x, 6),
];
}
return new WP_REST_Response([
'ok' => true,
'base' => 'HKD',
'updated_at' => $rates['updated_at'],
'source' => $rates['source'],
'currencies' => $filtered,
], 200);
}
function galvox_exchange_single_handler($request) {
$from = strtoupper($request->get_param('from'));
$to = strtoupper($request->get_param('to'));
$currencies = galvox_exchange_currencies();
if (!isset($currencies[$from]) || !isset($currencies[$to])) {
return new WP_Error('invalid_currency', 'Unsupported currency', ['status' => 400]);
}
$rates = galvox_exchange_get_rates();
if (is_wp_error($rates)) return $rates;
$from_rate = ($from === 'HKD') ? 1.0 : (1.0 / ($rates['rates'][$from] ?? 1));
$to_rate = ($to === 'HKD') ? 1.0 : (1.0 / ($rates['rates'][$to] ?? 1));
$cross = $to_rate / $from_rate;
return new WP_REST_Response([
'ok' => true, 'from' => $from, 'to' => $to,
'rate' => round($cross, 6),
'updated_at' => $rates['updated_at'],
'source' => $rates['source'],
], 200);
}
// ========================================================================
// ===== Phase 2 redirects (added 2026-08-17) =====
add_action('template_redirect', function() {
$redirects = array(
'/travel-insurance-compare-2026/' => '/travel-insurance/',
'/flights/' => '/deals/flights/',
'/hotels/' => '/deals/hotels/',
'/leave-guide/' => '/guides/leave-guide/',
);
$current = $_SERVER['REQUEST_URI'];
foreach ($redirects as $from => $to) {
if (rtrim($current, '/') === rtrim($from, '/')) {
wp_redirect(home_url($to), 301);
exit;
}
}
});

台北係港人短途自由行最方便嘅地方:
機票 (香港
台北):
建議訂早機去 + 晚機返, 玩足 6 日 5 晚, 桃園機場 (TPE) 出來搭機場捷運到台北車站 35 分鐘, NT$160。
或者搭高鐵由桃園到台北車站, 22 分鐘, NT$200。的士直接去酒店大約 NT$1,200-1,500。

1. 台北 W 酒店 (W Taipei) — 信義區, 31 樓, 設計時尚, 房間 40 平方米起, 望住台北 101。1 人入住 NT$5,800/晚 (約 HK$1,500)。26 樓紫艷酒吧中港台明星最愛, 1 人去都自在。
2. 台北文華東方 (Mandarin Oriental Taipei) — 松山區, 鬧中取靜, 房間 50 平方米起, 浴室有圓形浴缸。1 人入住 NT$8,800/晚 (約 HK$2,300)。米芝蓮一星 Bencotto 意大利餐廳必試。
08:00 國泰早機出發 → 10:00 桃園機場 → 11:00 機場捷運 → 11:45 台北車站 → 12:30 酒店 check-in (W 酒店) → 14:00 台北 101 觀景台 (NT$600) → 17:00 信義區逛街 → 19:00 鼎泰豐信義店 (小籠包) → 21:00 象山夜景
08:00 酒店早餐 → 09:30 故宮博物院 (NT$350) → 13:00 士林官邸 + 士林市場午餐 → 15:00 國立臺灣博物館 → 17:30 酒店休息 → 19:00 欣葉台菜 (米芝蓮推薦) → 21:00 饒河街夜市 (胡椒餅, 藥燉排骨)
08:00 早餐 → 09:00 台鐵到瑞芳 (NT$76) → 10:00 九份老街 (阿妹茶樓, 芋圓) → 13:00 十分老街放天燈 → 15:30 猴硐貓村 → 17:30 返台北 → 19:30 林東芳牛肉麵 → 21:00 酒店泳池
08:00 W 酒店早餐 → 10:00 退房 → 11:00 文華東方 check-in → 12:00 Bencotto 午餐 (米芝蓮一星) → 14:00 大安森林公園 → 15:30 永康街 (鼎泰豐總店) → 18:00 師大夜市 → 20:00 華山 1914 文創園區
08:00 文華東方早餐 → 09:30 捷運到北投 → 10:00 北投溫泉 (瀧乃湯, NT$400) → 12:30 北投市場 → 14:00 陽明山國家公園 (擎天崗, 冷水坑) → 17:00 草山夜未眠茶館 (夜景) → 19:30 返酒店 → 21:00 文華東方 Bencotto (米芝蓮)
08:00 酒店早餐 → 09:30 忠孝東路 SOGO / 新光三越 → 12:00 春水堂 (珍珠奶茶) → 14:00 酒店取行李 → 14:30 機場捷運 → 15:30 桃園機場 → 18:00 國泰晚機返港 → 19:30 抵達香港


1. 鼎泰豐 (信義/永康) — 小籠包 NT$220/籠, 米芝蓮推介。
2. 欣葉台菜 (信義) — 米芝蓮推薦, 套餐 NT$880 一位, 必食煎豬肝、菜脯蛋。
3. 林東芳牛肉麵 — 台北最強, 牛肉麵 NT$220, 宵夜首選。
4. 阿宗麵線 — 站著食嘅大腸麵線, NT$65, 西門町必去。
5. 春水堂 (信義) — 珍珠奶茶發明店, NT$150 一杯。
6. 饒河街夜市 — 胡椒餅 NT$50, 藥燉排骨 NT$90, 草莓糖葫蘆 NT$40。
7. 士林夜市 — 大餅包小餅 NT$50, 大香腸 NT$50, 鮮榨果汁 NT$60。
8. 北投市場 — 矮仔財滷肉飯 NT$50, 蔡元益紅茶 NT$30。
1. 台北 101 觀景台 (89 樓) — 必去, NT$600, 日景夜景都靚。
2. 九份阿妹茶樓 — 神隱少女場景, 黃昏最美, NT$300 一位茶位。
3. 象山自然步道 — 45 分鐘登頂, 望台北 101 全景, 免費。
4. 華山 1914 文創園區 — 舊酒廠改建, 文青最愛, 免費入場。
5. 大稻埕碼頭 — 黃昏日落, 迪化街散步, 影相一流。
6. 四四南村 — 信義區舊眷村, 文青 cafe, 望台北 101。

3-5 月 (春): 最佳, 氣溫 18-26 度, 唔太熱唔太凍, 雨季開始 (4 月起)。
6-9 月 (夏): 熱 (28-35 度), 7-9 月颱風季, 留意天氣, 部分景點可能關閉。
10-11 月 (秋): 最佳, 氣溫 22-28 度, 秋天最舒服。
12-2 月 (冬): 涼 (12-20 度), 雨多, 北投溫泉最佳季節。
Q: 港人免簽幾耐?
A: 30 日, 帶特區護照 + 回程機票就 OK, 唔需要簽證。
1. Q: 一個人食飯會唔會怪?
A: 台灣餐廳對 1 人食飯好友善, 鼎泰豐同欣葉都設有 1 人座位, 牛肉麵店 / 夜市 1 人食完全正常。
2. Q: 5 星酒店 1 人入住要加價?
A: W 酒店同文華東方都接受 1 人入住唔加價, 但早餐通常要加 NT$800-1,200 一位。
3. Q: 點聯絡?
台北旅遊熱線: 1999
觀光局網站: www.taiwan.net.tw

東澳島係珠海萬山群島入面最大嘅島, 距離香港只有 60 海里, 由港珠澳大橋 + 船程 1 小時 30 分鐘就到。阿麗拉酒店 2026 年 1 月開業, 係品牌喺中國第二間度假村 (第一間係三亞)。酒店有 122 間客房, 全海景設計, 最細客房都有 60 平方米, 落地窗外係成個南海。
酒店最大賣點係「懸崖泳池」, 25 米長無邊泳池突出喺懸崖上 8 米, 望出去只有海同天, 影相一流。仲有私人沙灘 (東澳島北灘), 沙幼水清, 適合浮潛。
由香港去東澳島阿麗拉, 路線分兩段:
第一段: 香港 → 珠海 (1 小時)
第二段: 珠海 → 東澳島 (1 小時 30 分鐘)

25 米無邊設計, 望住南海, 早晚影相都一流, 旁邊有 pool bar。
60 平方米起, 大部分有私人露台, 落地窗設計, 瞓醒就睇到海。
結合本地海草同火山石, 5 個療程房, 推薦「南海海草 Spa」(RMB 980, 90 分鐘)。
北灘沙幼水清, 提供免費浮潛用具 (面鏡、呼吸管、蛙鞋), 魚群豐富。
酒店提供免費島徑導賞 (3 公里, 1.5 小時), 沿途有戰時古堡、燈塔、椰林。
1. 北灘 — 島上最靚沙灘, 1.2 公里長, 沙幼水清, 早晚免費開放。
2. 蜜月閣 — 島上最高點, 登頂 30 分鐘, 望住成個南海同萬山群島。
3. 東澳灣戰時古堡 — 清代海防遺跡, 免費, 影相打卡一流。
4. 大竹灣燈塔 — 白色燈塔, 影日落一流, 行過去大約 40 分鐘。
5. 萬山島跳島 — 酒店可代訂船去萬山島, 30 分鐘船程, 食海鮮更便宜。


07:30 香港出發 → 09:00 港珠澳大橋 → 10:00 珠海口岸 → 11:00 香洲港碼頭 → 12:30 船程 → 14:00 酒店 check-in + 午餐 → 15:30 懸崖泳池 → 18:00 沙灘日落 → 19:30 酒店晚餐
08:00 海景早餐 → 09:30 島徑導賞 → 11:30 浮潛 → 13:00 酒店午餐 → 14:30 退房 → 15:00 碼頭 → 16:30 香洲港 → 17:30 珠海晚餐 (返大陸食海鮮) → 19:00 港珠澳大橋返港
1. 酒店海鮮餐廳 — 主打南海海鮮, 推薦椒鹽瀨尿蝦 (RMB 198) 同蒜蓉粉絲扇貝 (RMB 128)。
2. 島上漁家樂 — 東澳灣碼頭附近, 本地人經營, 海鮮便宜, 4 人 RMB 400 食到飽。
3. 香洲港附近海鮮酒樓 — 返大陸食, 性價比高, 推薦橫琴蠔 (RMB 38/打) 同白灼海蝦 (RMB 88/斤)。

4-6 月 (春): 最佳, 氣溫 22-28 度, 遊客少, 船票易買。
7-9 月 (夏): 旺季, 浮潛最佳, 但有颱風風險, 留意天氣預報。
10-11 月 (秋): 最佳, 氣溫 20-26 度, 影日落最靚。
12-3 月 (冬): 較涼 (12-18 度), 部分水上活動關閉, 但酒店有 35 度恆溫泳池。
Q: 港人需要簽證?
A: 持港澳通行證 + 有效期簽注即可入境大陸, 每次可停留 90 日。
Q: 船票點買?
A: 微信公眾號「香洲港」或者攜程買, 建議提前 3 日買, 旺季提前 1 星期。
Q: 帶小朋友去 OK 嗎?
A: 酒店有 kids club (4-12 歲), 免費, 沙灘淺水適合細路。
Q: 點聯絡酒店?
電話: 0756-2888 9999
微信公眾號: Alila Zhuhai

2026 年廈門島內新開 3 間精品酒店:
1. 廈門海岸線 W 酒店 (W Xiamen) — 2026 年 4 月開業, 位於思明區會展中心, 38 樓起, 全海景客房, 平均房價 RMB 1,580/晚。
2. 鼓浪嶼海灣安達仕 (Andaz Gulangyu) — 2026 年 5 月開業, 改建自百年歷史別墅群, 78 間客房, 庭院設計, 平均房價 RMB 2,180/晚。
3. 沙坡尾 K11 ARTUS 公寓酒店 — 2026 年 6 月開業, 結合藝文空間同服務式公寓, 適合長住或者家庭遊, 平均房價 RMB 1,280/晚。
香港西九龍站搭 G3002 次列車到廈門站, 全程 3 小時 55 分鐘。廈門站出來搭地鐵 1 號線或者的士 10 分鐘到思明區。
香港西九龍到深圳北 14 分鐘, 再轉 G1614 次列車到廈門北站 3 小時。深圳北轉車方便, 班次密。
香港飛廈門高崎機場, 飛行時間 1 小時 30 分鐘。機場到思明區 25 分鐘車程。機票連稅 HK$1,200-2,200 (旺季), 適合假期短嘅朋友。

鼓浪嶼係廈門必去, 渡輪由廈門輪渡碼頭出發, 航程 8 分鐘。建議提前 1 日網上買票 (RMB 35), 避免現場排隊。
1. 日光岩 (RMB 50) — 鼓浪嶼最高點, 早上 6 點開園, 登頂可睇成個廈門島同鼓浪嶼全景。8 點前去人少, 影相一流。
2. 菽莊花園 (RMB 30) — 海邊花園, 結合江南園林同西洋建築, 鋼琴博物館喺入面, 免費參觀。
3. 皓月園 (RMB 15) — 鄭成功雕像所在地, 望住大海, 影日落一流。
4. 風琴博物館 (RMB 20) — 全世界最大風琴收藏, 60 幾台古老風琴, 音樂愛好者必去。
5. 龍頭路小吃街 — 免費, 必食: 葉氏麻糍 (RMB 10/份), 餡餅 (RMB 8), 沙茶麵 (RMB 25)。
1. 八市海鮮市場 — 廈門最大傳統市場, 1 樓買海鮮 (代客加工), 2 樓食加工好嘅海鮮。一家大細人均 RMB 150-250。
2. 中山路黃則和 (百年老店) — 花生湯 (RMB 12), 餡餅 (RMB 8), 海蠣煎 (RMB 35), 便宜大碗。
3. 沙坡尾貓仔粥 — 廈門特色, 將米飯煮成粥再加入魷魚、蝦、肉丸, 鮮味濃郁, RMB 30 一碗。


3-5 月 (春): 最佳季節, 氣溫 18-25 度, 廈門鳳凰花未開但遊客少。
6-9 月 (夏): 熱 (28-35 度), 颱風季 (7-9 月), 留意天氣預報。鼓浪嶼渡輪可能停航。
10-11 月 (秋): 最佳季節, 氣溫 22-28 度, 鳳凰花開, 影相一流。
12-2 月 (冬): 涼 (10-18 度), 遊客最少, 部分海鮮店休息。
1. 沙坡尾避風塢 — 老廈門漁港, 彩色小屋, 文青最愛。
2. 廈門大學 (需預約) — 全國最美大學之一, 芙蓉湖、芙蓉隧道塗鴉牆必去。
3. 曾厝垵 — 漁村文創區, 小店林立, 夜晚有燈籠街, 氣氛一流。
4. 環島路白城海灘 — 免費, 日落時段最美, 踩單車或者散步都好。
5. 中山路騎樓夜景 — 百年騎樓建築群, 夜晚燈光璀璨。

Day 1: 08:00 香港出發 → 12:00 廈門站 → 13:00 酒店 check-in → 14:00 鼓浪嶼 (日光岩 + 菽莊花園) → 18:00 八市海鮮 → 20:00 中山路夜景 → 返酒店
Day 2: 08:00 酒店早餐 → 09:00 廈大 (預約入場) → 12:00 沙坡尾午餐 → 14:00 環島路踩單車 → 16:00 酒店取行李 → 17:00 廈門站 → 21:00 抵達香港
Q: 港人護照可以直接入場?
A: 鼓浪嶼渡輪同景點都可以, 鼓浪嶼渡輪票 RMB 35 對港人無優惠但接受港澳通行證購票。
Q: 廈大預約點樣做?
A: 微信公眾號「廈門大學」提前 3 日預約, 周末名額緊張建議平日去。
Q: 帶小朋友去鼓浪嶼有冇 BB 車租借?
A: 渡輪碼頭有, RMB 20/日, 押金 RMB 200。
Q: 點聯絡?
廈門旅遊熱線: 0592-12301
微信公眾號: 廈門旅遊
講到江西 2024 年最 hit 嘅隱世景點,望仙谷(位於上饒)絕對係首選。成個景區藏身喺贛東山谷入面,以「懸崖民宿+古鎮+瀑布」為主題,被譽為「江南小重慶」,風景似足武俠小說入面嘅世外桃源,人均 600 起,2 日 1 夜剛剛好。

望仙谷有 3 個必睇:

景區入面有 2 個住宿選擇:

望仙谷附近 3 個必去景點:

景區古鎮街有 20+ 間食肆,主打贛菜,推薦:望仙谷臘肉、上饒雞腿、弋陽年糕,人均 80 食到飽。想食多選擇可以自駕 30 分鐘去上饒市區(AMap 搜),推薦:弋陽龜峰扣肉、上饒米粉、鉛山燙米粉,人均 60。

小提醒:望仙谷 2024 年新開,部分設施仲未完善,但整體已超 5 星;高鐵站(上饒站)離景區 1 小時車程,call 的士 200 元;景區蚊多,帶蚊怕水;雨季(4-6 月)瀑布最靚但路滑,小心跣親。
關鍵詞:江西望仙谷、上饒隱世景點、望仙谷懸崖民宿、武俠夢景點、2 日 1 夜深度遊。
]]>講到珠海最 hit 嘅親子 staycation 酒店,海泉灣帆酒店絕對係 2024 年最 hit 之一。坐落喺珠海海泉灣度假區,一站過集齊海景+温泉+兒童樂園,人均 600 起就搞得掂,爸媽同小朋友都開心。

酒店有 4 種主要房型:

酒店親子配套超齊全:
酒店本身就喺海泉灣度假區,出門可達海泉灣温泉中心(人均 168,室內外 108 個池);想出海可以自駕去外伶仃島(車程 1.5 小時+船程 1 小時),島上玩浮潛/食海鮮;愛古風可以行圓明新園(車程 30 分鐘),皇家園林,影相打卡一流。

由香港出發最方便係高鐵到珠海站(約 1 小時 10 分,100 元起),轉的士去酒店約 40 分鐘(120 元);亦可由香港港澳碼頭坐船到珠海九洲港(1 小時,200 元起),再 call 的士 30 分鐘。經港珠澳大橋自駕亦非常方便,1.5 小時直達。

小提醒:海泉灣度假區位於珠海西邊,離市區遠,自駕最方便;園區入面有免費穿梭巴士,5 分鐘一班;酒店親子主題房只有 30 間,旺季成日爆,提早 1 星期訂。
關鍵詞:珠海海泉灣、帆酒店親子、珠海溫泉酒店、帶娃珠海 staycation。
]]>講到廣東 2024 年最 hit 嘅親子 staycation 酒店,陽春 DS 温泉度假酒店(位於陽春)絕對係首選。坐落喺陽春春灣風景區,成個度假村以「水城」為主題,入面有大型泳道+温泉池+兒童樂園,被譽為「廣東版威尼斯水城」,人均 500 起就搞得掂。

酒店有 4 種主要房型:

度假區水城配套超齊全:

酒店本身已經喺春灣風景區,出門可達春灣石林(步行 10 分鐘),廣東版「小三峽」,影相一流;愛探險可以自駕去凌霄巖(車程 15 分鐘),溶洞+地下河+玉簾洞,壯觀一流;想行山可以行鵝凰飄雪(車程 30 分鐘),廣東第二高峰,風景一流。

酒店餐廳主打粵菜+陽江特色,推薦:陽江魚飯、春灣豆腐、陽春砂仁雞,味道 85 分(人均 150)。想食地道啲可以自駕 10 分鐘去春灣鎮老街(AMap 搜),推薦:春灣米粉、陽春粽、砂仁雞湯,人均 60 食到飽。

小提醒:水城夏天(5-10 月)係旺季,兒童水寨成日爆;泳道房只有 50 間,提早 1 星期訂;酒店位置比較隱世,Google Maps 唔一定導得準,記得用 AMap 定位「陽春 DS 温泉」;自駕最方便,景區有 200 個車位。
關鍵詞:陽春 DS 温泉、廣東威尼斯水城、陽江親子酒店、暑假遛娃推薦。
]]>講到珠海 2024 年最 hit 嘅偽出國酒店,珠海鳳凰灣悦椿酒店(Anantara)絕對係首選。坐落喺鳳凰灣半島,以泰式+東南亞風設計,成個度假村被熱帶植物包圍,出門就係私家沙灘,泳池面對大海,真係好似去咗布吉/蘇梅。

酒店有 4 種主要房型:

酒店配套齊全:

酒店本身已經喺鳳凰灣,出門可達鳳凰灣海濱公園(步行 5 分鐘),沙灘一流;想出海可以自駕去外伶仃島(車程 1.5 小時+船程 1 小時),玩浮潛/食海鮮;愛古風可以行圓明新園(車程 30 分鐘),皇家園林,影相打卡一流。

酒店餐廳主打粵菜+泰菜,推薦:泰式冬蔭功、咖哩蟹、海南雞飯,味道 90 分(人均 350)。想食地道啲可以自駕 15 分鐘去灣仔海鮮街(AMap 搜),推薦:灣仔蠔、椒鹽瀨尿蝦、海膽炒飯,人均 150 食到飽。

小提醒:泳池別墅只有 15 間,旺季成日爆,提早 1 個月訂;酒店離市區遠,自駕最方便;園區入面蚊多,帶定蚊怕水;SPA 一定要預約,walk-in 冇位。
關鍵詞:珠海鳳凰灣、悦椿酒店 Anantara、珠海偽出國、東南亞風 staycation。
]]>講到廣東 2024 年最 hit 嘅 staycation 酒店,清遠新世界酒店(位於清遠清城區)絕對係首選。坐落喺清遠市中心,主打「城市度假」概念,5 星級配套但價錢親民,人均 700 起就搞得掂,家庭/情侶/閨密皆宜。

酒店有 4 種主要房型:

酒店配套超齊全:

酒店本身就喺清遠市中心,出門可達古龍峽(車程 30 分鐘),「廣東第一峽」,漂流一流;愛行山可以去牛魚嘴(車程 20 分鐘),原始森林+玻璃棧道;想慢活可以銀盞温泉(車程 20 分鐘),老牌温泉度假區。
酒店餐廳主打粵菜+自助餐,推薦:清遠雞(白切)、鴿子、鱔魚、艇仔粥,味道 90 分(人均 200)。想食地道啲可以自駕 10 分鐘去清遠市區(AMap 搜),推薦:清遠白切雞、刀切糍、洲心大粥,人均 50-100 食到飽。

小提醒:清遠高鐵站到酒店約 30 分鐘車程,免費穿梭巴士要預約;停車位 200 個(免費);酒店有提供清遠温泉/漂流套票,前台可以買,人均 300 起;古龍峽夏天爆滿,提早訂票。
關鍵詞:清遠新世界酒店、清遠 staycation、清遠雞、古龍峽漂流、廣東周末遊。
]]>講到深圳 2024 年最 hit 嘅親子度假酒店,深圳大梅沙京基洲際度假酒店(位於鹽田)絕對係首選。坐落喺大梅沙海濱,主打「沙灘+泳池+親子」3 大主題,有私家沙灘+無邊泳池+兒童樂園,人均 1200 起,家庭/情侶/閨密皆宜。

酒店有 5 種主要房型:

酒店配套超齊全:

酒店本身已經喺大梅沙,出門可達大梅沙海濱公園(步行 5 分鐘),免費開放,沙灘一流;想慢活可以行海濱棧道(步行 10 分鐘),沿海邊行,風景一流;愛刺激可以去東部華僑城(車程 20 分鐘),茶溪谷+雲中三俠+瀑布。

酒店餐廳主打粵菜+海鮮自助,推薦:海鮮自助餐(週日晚餐)、粵式點心、烤乳鴿,味道 90 分(人均 350)。想食地道啲可以行去大梅沙海鮮街(步行 10 分鐘),推薦:鹽田腸粉、海鮮砂鍋粥、烤生蠔,人均 100 食到飽。

小提醒:酒店旺季節假日(7-8 月)成日爆滿,提早 1 個月訂;私家沙灘 6-9 月開放,其他季節關閉;停車位 200 個(60 元/日);酒店有提供東部華僑城套票,前台可以買,人均 400 起;夏天去要防曬。
關鍵詞:深圳大梅沙、洲際度假酒店、鹽田親子酒店、無邊泳池酒店、深圳親子 staycation。
]]>講到珠海情侶路最 hit 嘅海景酒店,Palm Music 棕樂酒店(位於情侶路旁)絕對係 2024 年最 hit 之一。酒店以「棕櫚樹+音樂」為主題,大堂同房間都係南洋風裝修,部分房間有海景浴缸,浸住浴缸對住情侶路日落,氣氛一流。

酒店有 4 種主要房型:

酒店配套齊全:

酒店本身就喺情侑路旁邊,出門過馬路就到海濱,行 30 分鐘可達珠海漁女(珠海地標),影相一流;鍾意古風可以行圓明新園(車程 20 分鐘),按 1:1 比例重建嘅皇家園林;愛購物就去華髮商都(車程 10 分鐘),食飯 shopping 一條龍。

由香港出發最方便係高鐵到珠海站(約 1 小時 10 分,100 元起),轉的士去酒店約 20 分鐘(50 元);亦可由香港港澳碼頭坐船到珠海九洲港(1 小時,200 元起),再 call 的士 15 分鐘。經港珠澳大橋自駕亦非常方便,1.5 小時直達。

小提醒:海景浴缸房只有 20 間,旺季成日爆,提早 1 星期訂;情侶路假日塞車嚴重,自駕要預時間;酒店附近好食嘅餐廳唔多,建議食酒店餐廳或者 call 外賣。
關鍵詞:珠海情侶路酒店、Palm Music 棕樂、珠海海景 staycation、情侶 staycation 推薦。
]]>