PageSourceSearch

https://www.rushout.jp/js/ro-notice.js

js rushout.jp collected 2026-10-05 01:53:07 UTC 37,262 bytes, 698 lines download raw bytes

1/* ============================================================================
2 * 「ページが見つかりません」ページの noindex(=ソフト404の後始末)
3 * 2026-08-08 作成 / まだ本番には入れていない
4 *
5 * 【何をするものか】
6 *   削除済み商品のURL(例 /SHOP/tstp25062249.html)を開くと、ショップサーブは
7 *   「ご指定のページは見つかりません。」という文面を出す。ところが**HTTPの状態は
8 *   200(正常)**のまま。検索エンジンから見ると「正常なページがある」ことになるので、
9 *   いつまでも巡回対象に残り続ける(これをソフト404と呼ぶ)。
10 *   本当は 404 を返したいが、ショップサーブの商品ページはサーバー側の仕組みで
11 *   作られていて、こちらからは状態コードを変えられない(/SHOP/ はFTPにも無い)。
12 *   そこで次善の策として、**見つからないページのときだけ**
13 *     <meta name="robots" content="noindex,follow">
14 *   を入れて「このページは索引に入れないでください」と検索エンジンに伝える。
15 *   あわせて、既にある「見つかりません」の文の下に、次に進める案内を1つ足す。
16 *
17 * 【いちばん大事な約束】
18 *   **販売中の商品ページには絶対に入れない。**
19 *   まちがって販売中の商品に noindex を入れると、その商品が検索から消える=売上が落ちる。
20 *   そのため下の isNotFoundPage() は、条件を **5つ全部** そろえてから true を返す。
21 *   5つ目は「ショップサーブ自身が『見つかりません』と書いていること」で、これがいちばん強い。
22 *   1つでも欠けたら何もしない。
23 *
24 * 【止め方】
25 *   下の NOINDEX を false にして置き直す(再構築は不要・約5分で反映)。
26 *   丸ごと元に戻すなら、この行から「ここまで」の行までを消せばよい。
27 * ========================================================================== */
28(function () {
29  'use strict';
30
31  var NOINDEX = true;   // false にすると noindex を入れなくなる
32  var MESSAGE = true;   // false にするとお客様向けの案内を足さなくなる
33
34  // ショップサーブが出す「見つかりません」の文面。これがあることを必須条件にする。
35  // 実測: 空ページに1回だけ出現/販売中の商品ページには0回(2026-08-08)
36  var NOT_FOUND_TEXT = 'ご指定のページは見つかりません';
37
38  if (window.__roGone404Ran) { return; }
39  window.__roGone404Ran = 1;
40
41  /* ショップサーブの「見つかりません」表示が入っている箱を返す。無ければ null。 */
42  function findNotFoundBox() {
43    var boxes = document.querySelectorAll('#main-column .centerbox, .centerbox');
44    for (var i = 0; i < boxes.length; i++) {
45      var txt = boxes[i].textContent || '';
46      if (txt.indexOf(NOT_FOUND_TEXT) >= 0) { return boxes[i]; }
47    }
48    return null;
49  }
50
51  /* このページが「見つからないページ」かどうかを判定する。
52     5つの条件を **すべて** 満たしたときだけ true。 */
53  function isNotFoundPage() {
54    // (1) /SHOP/なんとか.html の形か。**かつ商品番号らしいこと(数字を含む)**
55    //     2026-08-09 追加: 数字を求めないと /SHOP/basket.html(カート)にも入った。
56    //     カートは中身が空のとき「見つかりません」の同じページを返すため、
57    //     下の(2)〜(5)を全部満たしてしまう。実測で確認。
58    //     商品番号は必ず数字を含む(par26041198 / ts55061141 / swtr14021076)ので
59    //     これで分かれる。basket・mailmag・wishlist などは数字が無いので入らない。
60    if (!/^\/SHOP\/[A-Za-z0-9_-]+\.html$/.test(location.pathname)) { return false; }
61    if (!/[0-9]/.test(location.pathname.replace(/^\/SHOP\//, ''))) { return false; }
62
63    // (2) タイトルが空か。販売中の商品は必ず商品名がタイトルに入っている。
64    //     ※ /SHOP/mailmag.html(メルマガ)等、商品でない実在ページはここで除外される
65    if ((document.title || '').replace(/[\s ]/g, '') !== '') { return false; }
66
67    // (3) 構造化データ(商品情報のかたまり)が無いか。販売中の商品には必ずある
68    if (document.querySelector('script[type="application/ld+json"]')) { return false; }
69
70    // (4) カートまわり・画像拡大フォームが無いか。
71    //     実物のHTMLに合わせてある:
72    //       <div class="cartArea">   … クラス。id ではない
73    //       <div id="cartButton…">   … id。番号が付くことがあるので前方一致
74    //       <form action="/SHOP/zoomitem.php"> … 画像拡大フォーム
75    //     (売り切れ表示の商品では cartButton が消えることがあるが、
76    //       cartArea と zoomフォームは残る)
77    if (document.querySelector(
78          '.cartArea, [id^="cartButton"], form[action*="zoomitem.php"]'
79        )) { return false; }
80
81    // (5) ショップサーブ自身が「見つかりません」と書いているか(最重要・唯一の積極的な証拠)
82    if (!findNotFoundBox()) { return false; }
83
84    return true;
85  }
86
87  function addNoindex() {
88    var m = document.querySelector('meta[name="robots"]');
89    if (m) {
90      // 既にあるなら、noindex が入っていないときだけ足す(黙って無効にならないように)
91      var c = m.getAttribute('content') || '';
92      if (c.toLowerCase().indexOf('noindex') < 0) {
93        m.setAttribute('content', c ? c + ',noindex' : 'noindex,follow');
94      }
95      return;
96    }
97    m = document.createElement('meta');
98    m.setAttribute('name', 'robots');
99    m.setAttribute('content', 'noindex,follow');
100    (document.head || document.documentElement).appendChild(m);
101  }
102
103  /* 既にある「見つかりません」の文の下に、次に進める案内を1つ足すだけ。
104     文面を二重に出さないため、新しい箱は作らない。 */
105  function addMessage(box) {
106    if (!box || document.getElementById('ro-gone-notice')) { return; }
107
108    var wrap = document.createElement('p');
109    wrap.id = 'ro-gone-notice';
110    wrap.style.cssText = 'margin:20px 0 0;line-height:1.9;font-size:14px';
111
112    wrap.appendChild(document.createTextNode(
113      '売り切れのため掲載を終了した可能性があります。'
114      + '古着は一点かぎりのため同じ商品の再入荷はありませんが、似たアイテムは毎日入荷しています。'));
115    wrap.appendChild(document.createElement('br'));
116
117    var a = document.createElement('a');
118    a.href = '/SHOP/list.php';
119    a.style.cssText = 'display:inline-block;margin-top:14px;padding:.8em 1.8em;'
120      + 'background:#111;color:#fff;text-decoration:none;border-radius:4px';
121    a.appendChild(document.createTextNode('商品一覧から探す'));
122    wrap.appendChild(a);
123
124    box.appendChild(wrap);
125  }
126
127  function run() {
128    try {
129      if (!isNotFoundPage()) { return; }
130      if (NOINDEX) { addNoindex(); }
131      if (MESSAGE) {
132        addMessage(findNotFoundBox());
133        // タブの表示が空欄のままだと分かりにくいので題名も入れる
134        // (※判定はこれより前に済ませてあるので影響しない)
135        document.title = 'ページが見つかりません|古着通販 RushOut';
136      }
137    } catch (e) {
138      // 何があっても店の表示は壊さない。ただし黙って壊れ続けないよう記録は残す
139      if (window.console && console.warn) { console.warn('ro-soft404:', e); }
140    }
141  }
142
143  // このファイルは </body> の直前で読まれるが、読まれる位置に左右されないよう
144  // 「HTMLを最後まで読み終わってから」判定する。
145  // (構造化データ ld+json はこのscriptタグの直後にあるため、待たずに判定すると
146  //   販売中の商品でも「ld+jsonが無い」と誤って見えてしまう)
147  if (document.readyState === 'loading') {
148    document.addEventListener('DOMContentLoaded', run);
149  } else {
150    run();
151  }
152})();
153/* ===== 「ページが見つかりません」ページの noindex ここまで ===== */
154
155
156/*!
157 * 偽サイト注意喚起バナー  /js/ro-notice.js
158 * 2026-08-01 設置 / 2026-08-02 作り直し
159 *
160 * 【このファイルの役割】
161 *   全ページの固定ヘッダー直下に「偽サイトにご注意ください」の帯を出す。
162 *   読み込む側は <script src="/js/ro-notice.js"></script> の1行だけなので、
163 *   文言・リンク・色を変えたいときは **このファイルを書き換えるだけ**。
164 *   お店ページの更新(再構築)は不要。
165 *
166 * 【バナーをやめるとき / 戻すとき】
167 *   下の SHOW を false(やめる)/true(戻す)にして
168 *   python deploy_ro_notice.py でアップロードし直すだけ。再構築は不要。
169 *
170 * ★いまの状態: SHOW = false(2026-08-08 に停止。帯は出ていません)
171 *   文言・リンク・色・仕組みはすべて残してあるので、true に戻せばそのまま復活します。
172 *
173 * 【なぜ「決まった名前で探す」のをやめたか】2026-08-02
174 *   本店はページごとに骨組みが違うことが実測で分かった。
175 *     トップ・カテゴリ・商品・お知らせ … <header> がある
176 *     カート・レジ                    … <header> は無く <div id="cart-header">
177 *     ログイン                        … <header> も #contents も無い
178 *   前の版は querySelector('header') だけを見ていたので、
179 *   **カードを入力するレジ画面でだけ帯が固定ヘッダーの裏に隠れていた**。
180 *   そこで名前で探すのをやめ、画面の一番上に居座っている要素を実際に測って
181 *   その真下に置く。テンプレートが増えても直さなくてよい。
182 *
183 *   本文の押し下げも #contents ではなく body の上余白で行う。
184 *   スマホのカートは #contents の margin-top が !important で、
185 *   JSから入れても無言で効かなかったため。
186 */
187(function () {
188  // 2026-08-08 笹谷さん指示で帯を止めた(false = 出さない)。
189  // 戻すときは true に直して python deploy_ro_notice.py。再構築は不要。
190  // 文言・リンク・色はそのまま残してあるので、true にすれば元どおり出る。
191  var SHOW  = false;                                // true にすると出る
192  var LINK  = '/hpgen/HPB/entries/914.html';
193  var TEXT  = '【重要】当店を装った偽サイトにご注意ください';
194  var BG    = '#c0392b';
195  var MAXHD = 260;      // これより高いものはヘッダーとみなさない(メニューの覆い等)
196
197  if (!SHOW) { return; }
198  if (window.__roNoticeRan) { return; }   // 2か所から読まれても動くのは1回だけ
199  window.__roNoticeRan = 1;
200
201  // 最初に1回だけ決めた「ヘッダー」と「押し下げ先」を覚えておく入れもの
202  var st = { hd: null, push: null, w: 0, ready: false };
203  // 告知ページ自身には出さない(そこにいるので不要)
204  if (location.pathname.indexOf('/entries/914.html') >= 0) { return; }
205
206  function make() {
207    if (!document.body) { return null; }
208    var d = document.getElementById('ro-notice-bar');
209    if (d) { return d; }
210    d = document.createElement('div');
211    d.id = 'ro-notice-bar';
212    d.setAttribute('role', 'region');
213    d.setAttribute('aria-label', '重要なお知らせ');
214    d.style.cssText = 'background:' + BG + ';color:#fff;text-align:center;'
215      + 'font-size:14px;line-height:1.5;box-sizing:border-box';
216    var a = document.createElement('a');
217    a.href = LINK;
218    a.appendChild(document.createTextNode(TEXT));
219    // 帯のどこを押しても飛べるように、余白は帯ではなくリンク側に付ける
220    // (帯側に付けると、押せるのは中央の21pxだけになる)
221    a.style.cssText = 'color:#fff;text-decoration:underline;display:block;'
222      + 'padding:.7em 1em;box-sizing:border-box';
223    d.appendChild(a);
224    document.body.insertBefore(d, document.body.firstChild);
225    return d;
226  }
227
228  function midX() { return Math.floor((window.innerWidth || 320) / 2); }
229
230  /* 画面の上部に居座っている固定ヘッダーの「要素そのもの」を返す。無ければ null。
231     縦に何点か当てて探す。トップページは最上部に別の帯(#ro-okaeri・90px)が
232     流し込みで入っていて、ヘッダーはその下に top:90px で固定されているため、
233     1点だけ見ると見落とす(2026-08-02 実測)。 */
234  function findHeader(bar) {
235    var keep = bar.style.display;
236    bar.style.display = 'none';          // 自分が邪魔をしないよう一旦隠して測る
237    var best = null, bottom = 0;
238    var vw = window.innerWidth || 320;
239    try {
240      for (var y = 2; y <= MAXHD; y += 8) {
241        var el = document.elementFromPoint(midX(), y);
242        while (el && el !== document.body && el !== document.documentElement) {
243          var pos = '';
244          try { pos = window.getComputedStyle(el).position; } catch (e) { }
245          if (pos === 'fixed' || pos === 'sticky') {
246            var r = el.getBoundingClientRect();
247            // 「画面いっぱいに広がる・上のほうにある・高すぎない」ものだけを
248            // ヘッダーとみなす。追従メニューやチャットの窓を拾わないため
249            if (r.top <= 200 && r.height > 0 && r.height <= MAXHD
250                && r.width >= vw * 0.6 && r.bottom > bottom) {
251              bottom = r.bottom;
252              best = el;
253            }
254          }
255          el = el.parentElement;
256        }
257      }
258    } catch (e) { }
259    bar.style.display = keep;
260    return best;
261  }
262
263  /* 帯の真下にある「本文のかたまり」を返す。ここに余白を足して本文を下げる。
264     ページによって押す相手が違う(トップ=#wrapper / カート=#contents)ので、
265     名前で決め打ちせず、実際に見えているものからさかのぼって探す。
266
267     hb = ヘッダーの下端(本文はここから始まっている)
268     bh = 帯の高さ(探る点は帯より下にする。帯自身を掴まないため)      */
269  function pushTarget(hb, bh) {
270    try {
271      var el = document.elementFromPoint(
272        midX(), Math.min(hb + bh + 4, (window.innerHeight || 600) - 2));
273      if (!el || el === document.body || el === document.documentElement) {
274        return null;
275      }
276      // 「上端がヘッダーの下端あたりから始まっている」いちばん外側の親までさかのぼる
277      while (el.parentElement && el.parentElement !== document.body
278             && el.parentElement !== document.documentElement) {
279        if (el.parentElement.getBoundingClientRect().top >= hb - 6) {
280          el = el.parentElement;
281        } else { break; }
282      }
283      // ヘッダーより上から始まっているもの、浮いているものは押してはいけない
284      if (el.getBoundingClientRect().top < hb - 6) { return null; }
285      var pos = window.getComputedStyle(el).position;
286      if (pos === 'fixed' || pos === 'sticky') { return null; }
287      return el;
288    } catch (e) { return null; }
289  }
290
291  /* 前回足した余白を外す。元々インラインで入っていた値はそのまま戻す */
292  function clearPush() {
293    var p = st.push;
294    if (p && p.el && p.el.style) {
295      try {
296        p.el.style.removeProperty('padding-top');
297        if (p.was) { p.el.style.setProperty('padding-top', p.was, p.pri); }
298      } catch (e) { }
299    }
300    st.push = null;
301  }
302
303  /* 余白は必ず padding で足す。margin だと親子で相殺されて
304     「入れたのに動かない」ことがある(2026-08-02 実測。#wrapper に margin を
305     入れても、子の #contents が持つ 158px と相殺されて1pxも動かなかった)。 */
306  function applyPush(el, add) {
307    var was = el.style.getPropertyValue('padding-top');
308    var pri = el.style.getPropertyPriority('padding-top');
309    var base = parseFloat(window.getComputedStyle(el).paddingTop) || 0;
310    // !important 付きで入れる。テンプレート側の指定に負けないように
311    el.style.setProperty('padding-top', (base + add) + 'px', 'important');
312    st.push = { el: el, was: was, pri: pri };
313    window.__roNoticePush = st.push;     // 動作確認用
314  }
315
316  /* ------------------------------------------------------------------
317     【いちばん大事な決めごと】2026-08-02
318     ヘッダーと押し下げ先は **最初に1回だけ決めて、二度と選び直さない**。
319
320     前の版は、スクロールが止まるたびに「いま画面に見えているもの」から
321     押し下げ先を選び直していた。そのため下へ行って戻るたびに、商品名・価格・
322     お気に入りボタン・購入ブロックなど**関係のない要素に41pxの余白が入り**、
323     ページがガタついていた(検品で実測。商品詳細では2回に1回発生)。
324     幅が変わったとき(画面回転・ウィンドウ操作)だけ測り直す。
325     ------------------------------------------------------------------ */
326  /* ページの一番上にいるか。押し下げ先は「一番上にいるとき」しか正しく測れない。
327     一覧→商品→戻る や F5 では、読み込んだ瞬間に途中までスクロールされた状態に
328     なる。そのとき画面に映っているのはページ先頭の器ではなく、たまたまそこに
329     あった商品名やお気に入りボタンなので、掴むと関係ない要素に余白が入る
330     (2026-08-02 検品で実測。商品一覧を2100pxまで送ってF5すると5回中5回発生)。 */
331  function atTop() {
332    return (window.pageYOffset || document.documentElement.scrollTop || 0) <= 4;
333  }
334
335  /* 押し下げ先を決めてよいか。一番上にいることだけが条件。 */
336  function canPush() {
337    return atTop();
338  }
339
340  /* force = true のときは、行き先が決まらなくても body に入れて必ず押し下げる。 */
341  function doPush(bar, force) {
342    if (!st.hd || !bar) { return; }
343    var hb = st.hd.getBoundingClientRect().bottom;
344    var bh = bar.getBoundingClientRect().height || 0;
345    if (bh <= 0) { return; }
346    var t = pushTarget(hb, bh);
347    /* 行き先が見つからないうちは入れずに待ち、次の機会にまた探す。
348       慌てて body に入れると、最上部に空白ができたり、押したつもりで
349       本文が下がらなかったりする(iPhone SE幅のコラムで実測)。
350       ただし「読み込みが終わらないページ」で永久に押されないのも困る
351       (応答の返らない画像が1枚あるだけで、カートの本文が19.8px
352       帯の裏に隠れたままになった)ので、最後は force で body に入れる。 */
353    if (!t && !force) { return; }
354    applyPush(t || document.body, bh);
355    st.pushed = true;
356  }
357
358  function measure() {
359    var bar = make();
360    if (!bar) { return; }
361    clearPush();                          // まず素の状態に戻してから測る
362    st.pushed = false;
363    st.hd = findHeader(bar);
364    if (st.hd) {
365      bar.style.position = 'fixed';
366      bar.style.top    = st.hd.getBoundingClientRect().bottom + 'px';
367      bar.style.left   = '0';
368      bar.style.right  = '0';
369      bar.style.width  = 'auto';
370      bar.style.zIndex = '9998';          // ヘッダー(9999)のすぐ下。上げすぎると
371                                          // スマホのメニューやカートの覆いより上に出る
372      // まだ決められないときは保留する。条件がそろったときに入れる
373      if (canPush()) { doPush(bar); }
374    } else {
375      // 固定ヘッダーが無いページは、普通に一番上へ置けばよい
376      bar.style.position = 'static';
377      bar.style.width  = '100%';
378      bar.style.zIndex = 'auto';
379    }
380    st.w = window.innerWidth;
381    st.tries = (st.tries || 0) + 1;
382    // ヘッダーがまだ固定されていない段階で測ってしまうことがあるので、
383    // 見つかるまでは「決まった」ことにしない(ただし探し続けすぎない)
384    st.ready = !!st.hd || st.tries >= 20;
385    // 動作確認用。測り直しが何度も起きていないかを外から見られるように
386    st.n = (st.n || 0) + 1;
387    window.__roNoticeMeasures = st.n;
388    window.__roNoticeLog = (window.__roNoticeLog || []).concat([{
389      n: st.n, w: st.w, hd: st.hd ? (st.hd.tagName + (st.hd.id ? '#' + st.hd.id : ''))
390                                  : 'なし',
391      push: st.push ? (st.push.el.tagName + (st.push.el.id ? '#' + st.push.el.id : ''))
392                    : 'なし'
393    }]);
394  }
395
396  /* スクロール中はヘッダーの位置だけを追う。押し下げ先は選び直さない。 */
397  function track() {
398    try {
399      if (!st.hd) { return; }
400      var bar = document.getElementById('ro-notice-bar');
401      if (!bar) { return; }
402      if (!document.documentElement.contains(st.hd)) {
403        // ページ側のJSにヘッダーを作り替えられた。次の機会に測り直す
404        st.hd = null; st.ready = false; return;
405      }
406      var b = st.hd.getBoundingClientRect().bottom;
407      if (b >= 0 && b <= MAXHD * 2) { bar.style.top = b + 'px'; }
408      // 保留していた押し下げを、条件がそろったここで入れる
409      if (!st.pushed && canPush()) { doPush(bar); }
410    } catch (e) { }
411  }
412
413  function fit() {
414    try {
415      // 幅が数pxだけ動いた(スクロールバーの出入りなど)程度では測り直さない。
416      // 測り直すたびに画面がガタつくため
417      if (!st.ready || Math.abs(window.innerWidth - st.w) > 8) { measure(); }
418      else { track(); }
419    } catch (e) { /* 何があっても本店の表示は壊さない */ }
420  }
421
422  /* ヘッダーには transition が付いていて、スクロール後もしばらく動き続ける。
423     止まった瞬間に1回測るだけだと、動いている途中の値で固定されてしまうので、
424     しばらくのあいだ毎フレーム追いかける。 */
425  var until = 0, running = false;
426  function chase() {
427    running = true;
428    track();
429    if (Date.now() < until) { requestAnimationFrame(chase); }
430    else { running = false; }
431  }
432  function onScroll() {
433    until = Date.now() + 600;
434    // running は chase が動いてから立つので、ここで先に立てないと
435    // 同じ瞬間に来たスクロール1回ごとに追従ループが並走してしまう
436    if (!running) { running = true; requestAnimationFrame(chase); }
437  }
438
439  var timer = null;
440  function later() {                    // 連続で呼ばれても最後の1回だけ計算する
441    if (timer) { clearTimeout(timer); }
442    timer = setTimeout(fit, 180);
443  }
444
445  if (document.readyState === 'loading') {
446    document.addEventListener('DOMContentLoaded', fit);
447  } else {
448    fit();
449  }
450  window.addEventListener('load', fit);
451  window.addEventListener('resize', later);
452  window.addEventListener('orientationchange', later);
453  window.addEventListener('scroll', onScroll, true);
454  setTimeout(fit, 300); setTimeout(fit, 1200); setTimeout(fit, 2500);
455
456  /* 最初の10秒だけは定期的に合わせ直す。
457     いまは gacha.js から非同期で読まれるので、動き出す頃にはヘッダーが
458     出来上がっている。しかし ShopServe の再構築が終わると、リマーケティング
459     タグ側の <script src> から **同期で** 読まれるようになり、2秒ほど早く
460     動き出す。そのとき上の3回のタイマーだけでは、ヘッダーが定位置に来る前の
461     値で固定され、赤い帯がヘッダーに重なったまま止まる(検品で実測)。
462
463     あわせて、押し下げが済むまでは見張りを続ける。カートのように読み込みが
464     重いページでは、10秒では間に合わずに押し下げが入らないことがあった。 */
465  var ticks = 0;
466  var iv = setInterval(function () {
467    ticks += 1;
468    fit();
469    // 保険。行き先が決まらないまま待たせ続けると、その間ずっと本文が
470    // 帯の裏に隠れる。読み込みが終わっていれば約5秒であきらめて body に入れ、
471    // 終わらないページでも28秒で必ず入れる
472    if (!st.pushed && st.hd && atTop()
473        && ((ticks === 12 && document.readyState === 'complete') || ticks === 70)) {
474      try { doPush(document.getElementById('ro-notice-bar'), true); } catch (e) { }
475    }
476    // 押し下げが済んで10秒たったら終わり。
477    // 固定ヘッダーが無いページ(ログイン等)は押し下げ自体が要らないので、
478    // それが確定した時点で終わり。どちらでもないときは最大30秒まで待つ
479    if ((st.pushed && ticks >= 25) || (st.ready && !st.hd) || ticks >= 75) {
480      clearInterval(iv);
481    }
482  }, 400);
483})();
484
485
486
487/* ==========================================================================
488   【記録】送料無料の金額の暫定修正について  2026-08-03
489
490   スマホでカートに進むと、上の帯だけが古い「7,990円以上お買い上げで送料無料」
491   のままだった(該当5ページ)。正しくは 7,999円(税込)以上。
492   根拠は店の案内5か所が一致(ご利用案内 / よくあるご質問 / 特定商取引法に
493   基づく表記 / ラッシュアウトはここが違う / メールマガジン)。
494   9円足りないのに「無料になる」と読める向きの誤りだった。
495
496   一時はここでJSから書き換えていたが、2026-08-03 に
497   管理画面 > スマートフォンサイト設定 > 各種設定 の
498   htmlHeader[shopTopHeader] / htmlHeader[shopItemHeader] を直したので
499   **応急処置は取り除いた**。
500
501   ※この画面の落とし穴: 設定は1項目につき2か所(見えている編集欄と、
502     フォーム内の隠し入力)にあり、**保存されるのは隠し入力のほう**。
503     片方だけ直すと保存しても元に戻る。詳しくは
504     shopserve_fix_smp_obi.py の説明を参照。
505   ========================================================================== */
506
507
508/* ==========================================================================
509   商品ページの「ポイント還元」を目立たせる  2026-09-02(笹谷さんご指示)
510
511   【何をするものか】
512     商品ページの価格の下に出る「[ポイント還元 199ポイント〜]」を、
513     スマホで **赤字・太字・少し大きく** する。
514
515   【なぜCSSだけなのか】
516     この文字はショップサーブが出しているもの(<span class="publish_point">)。
517     先方のテンプレートやCSSは書き換えず、**打ち消しのCSSを当てるだけ**にしてある
518     (2026-08-07 の「画像1枚の商品の大画像を戻すCSS」と同じやり方)。
519     置き場所は /docs/js/ なので、お店の再構築でも消えない。
520
521   【実物のCSSを見て分かったこと】(テーマCSS 14605-14615行)
522     ・この行は表ではなく **display:flex; justify-content:flex-end**
523     ・`.publish_point` は **font-size:14px**(絶対値)。だから em ではなく px で書く
524     ・`.publish_point` は既に **white-space:nowrap**。字を大きくすると折り返さずに
525       **左へはみ出して読めなくなる**ので、折り返せるように戻す
526     ・左にある見出しセル(th)は**中身が空**で幅40%を取っている。
527       表ではないので、隠しても価格の行はずれない
528     ・この店の区切り幅は **750px**(テーマCSSに350か所。767pxは0か所)
529
530   【止め方 / 変えかた】
531     ・やめる … TSUYOMERU を false にして deploy_ro_notice.py で置き直す
532     ・色 … IRO(いまの #c0392b は白地でコントラスト5.4:AA基準を満たす)
533     ・大きさ … OOKII_PX(元は14px)
534     ・PCにも出す … SP_DAKE を false
535     お店の更新(再構築)は不要。届くまで約5分。
536   ========================================================================== */
537(function () {
538  'use strict';
539
540  var TSUYOMERU = true;      // false にすると何もしない(元の見た目に戻る)
541  var SP_DAKE   = true;      // false にするとPCでも赤字・太字になる
542  var IRO       = '#c0392b'; // 赤
543  var OOKII_PX  = 16.8;      // 文字の大きさ(元は14px。1.2倍=16.8px)
544  var SP_HABA   = 750;       // この店の区切り幅(テーマCSSに合わせた)
545
546  if (!TSUYOMERU) { return; }
547  if (window.__roPointStyled) { return; }   // 二重に入れない
548  window.__roPointStyled = 1;
549
550  // ★商品ページの価格ブロック(#pi_cart)の中だけに当てる。
551  //   裸の .publish_point にすると、一覧やカートに同じ字が出ていたときに巻き添えになる
552  var moto = '#pi_cart table.price tr.rg_price ';
553  var kime =
554    // 左の空欄を隠して、本文に幅をゆずる(この行は flex なので価格の行はずれない)
555    moto + 'th{display:none !important;}' +
556    // ★この行は flex なので、中の箱は既定で縮む。縮むと折り返してしまうので縮ませない
557    //   (2026-09-02 笹谷さんご指示「改行されずに一行に」)
558    moto + 'td{flex:0 0 auto !important;}' +
559    moto + '.publish_point{' +
560      'color:' + IRO + ' !important;' +
561      'font-weight:700 !important;' +
562      'font-size:' + OOKII_PX + 'px !important;' +
563      // 折り返さず1行に収める(もとのテーマCSSも nowrap。空欄を隠したぶん幅は足りる。
564      //  いちばん狭い iPhone SE 320px でも、4桁ポイントで約260px と余裕がある)
565      'white-space:nowrap !important;' +
566      'line-height:1.5 !important;' +
567    '}';
568
569  var css = SP_DAKE
570    ? '@media screen and (max-width:' + SP_HABA + 'px){' + kime + '}'
571    : kime;
572
573  try {
574    var st = document.createElement('style');
575    st.setAttribute('data-ro', 'point-emphasis');
576    st.appendChild(document.createTextNode(css));
577    (document.head || document.documentElement).appendChild(st);
578  } catch (e) {
579    /* 何かあっても、お店の表示は元のまま。止めるだけにする */
580  }
581})();
582/* ===== 「ポイント還元」を目立たせる ここまで ===== */
583
584/* ===== 特集ページ経由の購入を数える ここから ==================================
585 * 2026-09-16 追加
586 *
587 * 【なぜ要るのか】
588 *   /popular/ /sweat/ /ralph/ の特集ページは、商品カードのクリックを GA4 の
589 *   select_item として「どの特集か」の名前付きで送っている。ところが商品ページから
590 *   先(カート・購入)はショップサーブ製で、その名前を引き継いでくれない。
591 *   実際 2026-09-16 時点で、カート投入 11,979件・購入 1,930件のすべてが
592 *   item_list_name「(not set)」になっており、**特集から何件売れたかが分からない**。
593 *
594 * 【やること】
595 *   特集ページでカードが押されたとき、あちら側が
596 *     ro_tokushu=<特集>.<商品コード>.<押した時刻(秒)>
597 *   というクッキーを置く。ここでは**購入完了ページのときだけ**それを読み、
598 *   GA4へ 1件送って、クッキーを消す。
599 *
600 * 【商品コードと経過分を一緒に送る理由】(2026-09-16 検品の指摘)
601 *   クッキーが残っている間の購入を全部「特集のおかげ」と数えると、必ず多く出る。
602 *   /popular/ を見る人の9割は既に店内を回っている人で、特集と関係なく何かを買う。
603 *   そこで **押した商品コード** を一緒に送っておき、あとで
604 *   「その商品が実際に売れたか」を突き合わせられるようにする(一点物なので確実)。
605 *   **経過分**も送るので、「何時間以内を特集の手柄とみなすか」をあとから決められる。
606 *   いま決め打ちすると、後から狭めても過去のデータを作り直せない。
607 *
608 * 【安全のために】
609 *   ・購入完了ページ以外では即座に何もせず抜ける(location.pathname で判定)
610 *   ・send_to でGA4だけに送る。**付けないとGoogle広告(AW-791164952)にも飛ぶ**
611 *     (この店は1つの dataLayer をGA4と広告で共有している。2026-09-16 実測)
612 *   ・クッキーの値は誰でも書けるので、知っている名前のときだけ動く
613 *     (hasOwnProperty で見る。'toString' などを渡されても動かない)
614 *   ・送れたときだけクッキーを消す。送れなければ次の購入まで残る
615 *   ・gtag がまだ読み込まれていないことがあるので、少し待って数回試す
616 *   ・二重に読み込まれても1回しか動かない(__roTokushuRan)
617 *   ・何があっても表示は変えない。売上に影響する処理は一切しない
618 *
619 * 【止め方】
620 *   下の IRU を false にして置き直す(再構築は不要・約5分で反映)
621 * ---------------------------------------------------------------------------- */
622(function () {
623  'use strict';
624
625  var IRU = true;                       // false にすると何もしない
626  var GA4 = 'G-0VFFKWHQNE';             // ここだけに送る(広告側へ飛ばさない)
627  var KANRYOU = ['/CART/order.php', '/CART/smp_order.php'];   // 購入完了ページ
628  var NAMAE = {                         // クッキーの合言葉 → GA4に送る名前
629    popular: '人気の古着 みんなが選んだ',
630    sweat: '秋のスウェット特集',
631    ralph: 'ラルフローレン特集'
632  };
633
634  if (!IRU) return;
635  if (window.__roTokushuRan) return;     // 2経路で読まれた日に二重に数えないため
636  if (KANRYOU.indexOf(location.pathname) < 0) return;
637  window.__roTokushuRan = 1;
638
639  var m = document.cookie.match(/(?:^|;\s*)ro_tokushu=([^;]*)/);
640  if (!m || !m[1]) return;
641  var bun = String(m[1]).split('.');
642  var kagi = bun[0];
643  if (!Object.prototype.hasOwnProperty.call(NAMAE, kagi)) return;
644  var namae = NAMAE[kagi];
645  var code = (bun[1] || '').slice(0, 40);
646  var toki = parseInt(bun[2], 10);
647  var fun = (toki > 0) ? Math.round((Date.now() / 1000 - toki) / 60) : -1;
648
649  var nokori = 20;                      // 0.3秒 × 20 = 最大6秒待つ
650  function okuru() {
651    if (typeof gtag !== 'function') {
652      if (--nokori > 0) { setTimeout(okuru, 300); }
653      return;
653                           // 送れなければクッキーは残す
654    }
655    try {
656      gtag('event', 'tokushu_purchase', {
657        send_to: GA4,
658        tokushu_list: namae,
659        tokushu_code: code,
660        tokushu_keika_fun: fun
661      });
662      // 送れたときだけ消す。再読み込みで二重に数えないため
663      document.cookie = 'ro_tokushu=;path=/;max-age=0;samesite=lax;secure';
664    } catch (e) {
665      /* 送れなくても、お店の表示は元のまま */
666    }
667  }
668  okuru();
669})();
670/* ===== 特集ページ経由の購入を数える ここまで ===== */
671
672/* ===== ラッシュアウト祭 全ページ中央ポップアップの読み込み(2026-09-30 追加・祭のあと外す) =====
673   中身は /slide-banner/js/matsuri_popup.js(元は ラッシュアウト祭\フェス案\matsuri_popup.js)。
674   2026/10/10 0:00〜10/16 0:00(日本時間)の間だけ読みに行く。それ以外の日は何もしない。
675   この上の帯・計測とは独立したかたまり。ここで何か起きても上には響かない(try で囲む) */
676(function () {
677  try {
678    var t = Date.now();
679    if (t < Date.UTC(2026, 9, 9, 15) || t >= Date.UTC(2026, 9, 15, 15)) return;
680    var add = function () {
681      try {
682        if (document.getElementById('rom-loader')) return;
683        var s = document.createElement('script');
684        s.id = 'rom-loader';
685        s.src = '/slide-banner/js/matsuri_popup.js';
686        s.async = true;
687        (document.head || document.documentElement).appendChild(s);
688      } catch (e) { /* 出せなくても、お店の表示は元のまま */ }
689    };
690    // ポップアップは body に足すので、ページの本文ができてから読む
691    if (document.readyState === 'loading') {
692      document.addEventListener('DOMContentLoaded', add);
693    } else {
694      add();
695    }
696  } catch (e) { /* 何もしない */ }
697})();
698/* ===== ラッシュアウト祭 全ページ中央ポップアップの読み込み ここまで ===== */

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.