週1共有会で進める!Web制作・フロントエンド改善の6事例

はじめに

みなさん、こんにちは。
プロモーション・デザインユニット(以下、プロモ・デザインU)の安田です。
私たちのチームはオプティムのWebサイト全体の管理・運用を担当しており、コンテンツの企画からページ制作まで幅広く手がけています。

前回はWebアクセシビリティについてお話ししました。
今回は今年度から始めた「Web制作チームの共有会」について、どういうテーマに関して話しているのか、そして「共有会をやって改善できた!」と実感したポイントをご紹介します。

Web制作チームの共有会導入(背景・課題)

オプティムではフロントエンド推進室やデザイン勉強会、営業ノウハウの共有会などテーマごとに有志で行っている活動が複数存在しています。
今回ご紹介するWeb制作チームの共有会はプロモ・デザインU内での困りごと、相談、共有などを中心に行なっている活動です。
参加メンバーはプロモ・デザインUでコード実装を担当する5名。週1回、月曜日の夜に45分〜1時間程度の時間を取って開催しています。

元々、プロモ・デザインUには以下のような課題を抱えていました。

  • コードのフォーマットがバラバラ。フォーマッターを入れている人と入れていない人で、修正箇所以外にも差分が発生してしまった。
  • 知識が属人化しており作業スピードや対応内容に大きくばらつきがある。
  • 技術力の高い人が書いたコードが読めず、運用できず、やむなく手放した社内運用アプリがある。
  • ヒヤリハット収集をしているが、その後の対応方針などが一部メンバーしか検討に関わっていなかった。

Web制作チームの共有会は、上記の課題解決の場として活動しています。また、昨年度からAIをプロモ・デザインUでも積極的に活用しており、AIツールの紹介や運用改善にも取り組んでいます。
改善できる内容をチケット化し、案件が落ち着いたタイミングで改善を続けています。

共有会で議論したトピック

これまでの共有会で議論したトピックをカテゴリ別に紹介します!

GitLab CI/CD

  • CI/CDを使った404チェックやテストサイトURL検知の自動化の検討
  • JavaScriptが絡む箇所の単体テスト実装方法の検討

GitLab 運用

  • ブランチ削除漏れを防ぐ自動削除機能の検討
  • ブランチ名変更(master→main)の方針整理
  • マージリクエスト(MR)で競合が起きたときの対応手順の整理

CSS・SCSS

  • font-sizeをピクセル指定せず、実装者が読み取りやすい記述方法の検討
  • コードフォーマッターを統一方針の整理

PHP / WordPress

  • 画像のレティーナ対応、PC・SP切り替えのHTMLを簡単に実装する関数の共有
  • WordPressの画像上書きアップロード関数の使い方説明
  • EOLしたNuxt.js Ver.2で構築されたサイトをPHPベースに作り替える検討

ツールの使い方

  • 社内で導入しているAIの使い方共有
  • おすすめのChromeアドオンやターミナルコマンドの共有
  • AIで作った自動化Pythonスクリプト(自動スクリーンショット、特定文字のスクレイピングなど)の使い方共有
    ※スクレイピングや自動取得は、対象サイトの利用規約・robots設定・取得データ(個人情報や社内情報)に注意し、許可された範囲で実施してください。

タスク化して改善した事例

タスク化したチケットベースで改善例の一部をご紹介します!

【GitLab】ブランチの自動削除

  • Before: ブランチの削除忘れで、たくさんのブランチが負の遺産と化していた…
  • How: ブランチ名にRedmineの番号を記載する運用にしているため、mainマージ時と月次(1日朝9時)にブランチ名からRedmineに情報を取得し、該当作業のステータスが完了であればブランチ自動削除
  • Result: 現在進行している作業の可視化。ブランチ削除工数の削減。
# .gitlab-ci.yml の例
# CIの資格情報(トークン等)をURLに直書きすると、ログやエラー出力から漏洩する可能性があります。運用時はMasked/Protected変数を利用し、ログに出ない方法で管理してください。
stages:
  - cleanup
cleanup:
  image: alpine:latest
  stage: cleanup
  before_script:
    - apk add --no-cache git openssh curl jq
    - mkdir -p ~/.ssh
    - git config --global user.name "CI Bot"
    - git config --global user.email "ci@example.com"
    - git remote set-url origin https://$GITLAB_USERNAME:$GITLAB_TOKEN@$CI_SERVER_HOST/$CI_PROJECT_PATH.git
  script:
    - chmod +x scripts/cleanup_branches.sh
    - ./scripts/cleanup_branches.sh "$REDMINE_API_KEY" "$TEAMS_WEBHOOK_URL" "$REDMINE_URL"
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: always
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: always
    - when: never

【GitLab】CI/CDでの単体テスト

  • Before: 公開されているWebサイトのJavaScriptがいつの間にか動かなくなっていた😇
  • How: GitLab CIの共通ジョブ化/テストレポートの可視化。
  • Result: 自身が更新した箇所以外で影響のある処理の単体テストチェックが可能に。
// tests/cost-simulator.spec.js の例
const { test, expect } = require("@playwright/test");

test("シミュレーターが正しく動作する", async ({ page }) => {
  await page.goto("<FEATURE_PATH>/?monthly_salary=5&yearly_contract=10&ratio_deadline_contract=10&ratio_search_contract=20&number_management=4");

  await page.waitForSelector(".result_value");
  const text = await page.textContent(".result_value");

  expect(text).not.toBe("");
  expect(Number(text.replace(/,/g, ""))).toBeGreaterThan(0);
});

test("パラメータが欠けている場合、結果は非表示になる", async ({ page }) => {
  await page.goto("<FEATURE_PATH>/?monthly_salary=5&yearly_contract=&ratio_deadline_contract=10&ratio_search_contract=20&number_management=4");

  // 結果エリアが非表示であることを確認
  const isVisible = await page.isVisible(".data-output");
  expect(isVisible).toBe(false);

  // 入力エリアが表示されていることを確認
  const isInputVisible = await page.isVisible(".data-input");
  expect(isInputVisible).toBe(true);
});

【GitLab / AIの使い方】PR Agent

  • Before: 実装部分の書き方を読み解くのに工数がかかっていた。エラーなども見逃しがちだった。
  • How: 全社で準備いただいたPR Agentをプロモチームのリポジトリに反映。
  • Result: レビュー作業が効率化できるようになった。変数の誤字脱字の削減ができた。

【CSS・SCSS】コードフォーマッター整備

  • Before: 共通フォーマットがないため、修正箇所以外でもフォーマットで差分が発生していた。
  • How: ESLint/Prettier/Stylelintの共通設定。
  • Result: MRでの不要差分削減、レビュースループット向上。
// settings.json の例
{
  // ========================
  // 🔤 Emmet 設定
  // ========================
  "emmet.variables": {
    "lang": "ja"                 // Emmetの変数langを日本語に設定(lang属性のデフォルト値)
  },
  "emmet.excludeLanguages": [
    "markdown"                   // Emmetを無効にする言語(Markdownでは補完しない)
  ],

  // ========================
  // 📝 エディタ全般
  // ========================
  "editor.wordWrap": "off",               // 自動折り返しを無効化
  "editor.wordWrapColumn": 10000,         // 折り返し列数(ほぼ折り返されない設定)
  "editor.wrappingIndent": "none",        // 折り返し行のインデントなし
  "editor.tabSize": 2,                    // インデントは2スペースに設定。
  "editor.formatOnSave": true,            // 保存時に自動で整形(フォーマット)を実行。
  "editor.codeActionsOnSave": {
    "source.fixAll.stylelint": "explicit" // 保存時に Stylelint を使って CSS/SCSS のスタイル修正を実行。
  },

  // ========================
  // 💾 ファイル保存
  // ========================
  "files.autoSave": "afterDelay",         // ファイルを一定時間後に自動保存。

  // ========================
  // 🚀 起動・UI・ワークベンチ
  // ========================
  "workbench.startupEditor": "none",        // 起動時にスタート画面を表示せず、空のウィンドウにする。
  "workbench.editorAssociations": {
    "*.svg": "default"                      // SVGファイルはデフォルトのエディタで開くように設定。
  },

  // ========================
  // 🎨 フォーマッタ / Linter 設定
  // ========================
  // HTML
  "[html]": {
    "editor.defaultFormatter": "vscode.html-language-features"
  },                                       // HTMLは標準機能で整形
  "html.format.wrapLineLength": 0,         // 折り返す行の長さを無制限に設定(折り返さない)
  "html.format.preserveNewLines": true,    // 元の改行を維持
  "html.format.maxPreserveNewLines": null, // 改行維持数の上限なし

  // CSS / SCSS
  "[css]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",  // PrettierでCSS整形
    "editor.formatOnSave": true                           // 保存時に自動整形
  },
  "[scss]": {
    "editor.formatOnSave": true                           // 保存時に自動整形
  },
  "scss.validate": false,                                 // SCSSのバリデーション無効(Stylelintに任せる)
  "css.validate": false,                                  // CSSのバリデーション無効(Stylelintに任せる)
  "stylelint.validate": [
    "css",
    "scss"
  ],                                                      // StylelintによるCSSとSCSSのバリデーションを有効化

  // JavaScript
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"   // JavaScriptもPrettierで整形。
  },

  // Prettier
  "prettier.proseWrap": "never",          // Markdownなどでも自動折り返ししない。

  // ========================
  // 🧠 PHP & Intelephense 設定
  // ========================
  "php.validate.enable": false,          // PHPの標準バリデーションを無効(Intelephenseを利用するため)。
  "php.suggest.basic": false,            // PHPの標準補完を無効化(Intelephenseを利用するため)。
  "intelephense.format.braces": "k&r",   // ()を改行しないようにする。
  "intelephense.stubs": [
    "apache", "bcmath", "bz2", "calendar", "com_dotnet", "Core", "ctype", "curl", "date", "dba", "dom", "enchant", "exif", "FFI", "fileinfo", "filter", "fpm", "ftp", "gd", "gettext", "gmp", "hash", "iconv", "imap", "intl", "json", "ldap", "libxml", "mbstring", "meta", "mysqli", "oci8", "odbc", "openssl", "pcntl", "pcre", "PDO", "pgsql", "Phar", "posix", "pspell", "random", "readline", "Reflection", "session", "shmop", "SimpleXML", "snmp", "soap", "sockets", "sodium", "SPL", "sqlite3", "standard", "superglobals", "sysvmsg", "sysvsem", "sysvshm", "tidy", "tokenizer", "xml", "xmlreader", "xmlrpc", "xmlwriter", "xsl", "Zend OPcache", "zip", "zlib", "wordpress"
  ],  // PHPで補完・解析対象とするモジュール(WordPressやPDOなど)を大量に指定。

  // ========================
  // 🔀 Git / Diff エディタ関連
  // ========================
  "git.autofetch": true,                          // リモート変更を自動でフェッチ。
  "git.ignoreMissingGitWarning": true,            // .git が見つからなくても警告しない。
  "git.openRepositoryInParentFolders": "never",   // 親フォルダにあるGitリポジトリは開かない。
  "diffEditor.wordWrap": "off"                   // 差分エディタでは折り返しを無効。
}

【CSS・SCSS】font-size関数

  • Before: px指定で書かれたもの。calcを使ったもの。rem指定で何ピクセルがわかりづらいものが混在。
  • Why: デバイス差/デザイン差異の吸収。
  • How: rem/calcベースのスケール統一
▼関数
$baseFontSize: 16;
@function rem($pixels) {
  @return $pixels / $baseFontSize * 1rem;
}

▼呼び出し方
font-size: rem(32);

▼出力
font-size: 2rem;

【PHP】WordPressの画像上書きアップロードの改良

  • Before: 画像の上書きアップロードプログラムを使っていたが、アイキャッチの更新やアップロード先の紐付けができていなかった。
  • How: DBから必要情報を取得し、アイキャッチやアップロード先も保持したまま更新するように修正。
  • Result: 画像の差し替え工数の大幅削減が可能に。
// functions.php の例
// コードは「例示」です。運用前にバックアップを取得し、ステージングで検証してください。
// この処理は既存画像を削除・再設定するため、必要があれば管理画面内かつ権限のあるユーザーに限定して動作するよう制御してください。

/**
 * WordPress における画像の上書きアップロード処理。
 *
 * 同名の画像がすでに存在する場合、既存の attachment を安全に削除し、
 * それをアイキャッチ、ACF画像フィールド、または post_parent に使っていた投稿に対して、
 * 新しい画像で再設定する。
 *
 * 処理の流れ:
 * 1. アップロード前に同名ファイルが存在するかを確認
 * 2. 同名画像が使用されていた投稿情報を記録(meta_key単位・post_parent)
 * 3. 対象の既存 attachment を削除
 * 4. アップロード完了後に、新しい画像で再設定を行う
 * 
 * 対象となるフィールド(meta_key)について:
 * - 画像ID(attachment ID)を保持しているフィールド名(meta_key)を
 *   $GLOBALS['image_meta_keys'] に列挙してください。
 *   例:'_thumbnail_id', 'image' など
 * - ACF・独自実装問わず、値が attachment ID であるメタフィールドは対象となります。
 * - post_parent の再設定は個別に対応しています。
 *
 * 参考:
 * @see https://spfx.jp/wordpress-replace-image-without-plugin/
 */

// 設定:画像IDを保持するmeta_keyの一覧
$GLOBALS['image_meta_keys'] = [
    '_thumbnail_id',
    'image',
    // 必要に応じて追加
];

// グローバル記録用
$GLOBALS['_replaced_attachment_ids'] = [];       // filename => [旧attachment IDs]
$GLOBALS['_image_meta_before_delete'] = [];      // 旧attachment ID => [meta_key => [投稿ID...]]
$GLOBALS['_parent_before_delete'] = [];          // 旧attachment ID => 親投稿ID

/**
 * アップロード前に同名の画像が存在するかを確認し、存在する場合は削除と事前処理を行う。
 *
 * @param array $file アップロードされるファイル情報
 * @return array 処理済みファイル情報
 */
add_filter('wp_handle_upload_prefilter', function ($file) {
    $filename = $file['name'];

    $attachments = get_posts([
        'post_type' => 'attachment',
        'meta_query' => [[
            'key'     => '_wp_attached_file',
            'value'   => $filename,
            'compare' => '=',
        ]],
        'posts_per_page' => -1,
    ]);

    if (empty($attachments)) {
        return $file;
    }

    foreach ($attachments as $attachment) {
        $attachment_id = $attachment->ID;

        // 削除予定のIDを記録(ファイル名単位)
        $GLOBALS['_replaced_attachment_ids'][$filename][] = $attachment_id;

        // 親投稿(post_parent)記録
        if ($attachment->post_parent) {
            $GLOBALS['_parent_before_delete'][$attachment_id] = $attachment->post_parent;
        }

        // 各meta_keyに対する使用投稿を記録
        foreach ($GLOBALS['image_meta_keys'] as $meta_key) {
            $posts = get_posts([
                'post_type' => 'any',
                'meta_query' => [[
                    'key' => $meta_key,
                    'value' => $attachment_id,
                    'compare' => '=',
                ]],
                'fields' => 'ids',
                'posts_per_page' => -1,
            ]);

            foreach ($posts as $post_id) {
                $GLOBALS['_image_meta_before_delete'][$attachment_id][$meta_key][] = $post_id;

                // アイキャッチは一旦削除しておく(meta_key = _thumbnail_id)
                if ($meta_key === '_thumbnail_id') {
                    delete_post_meta($post_id, $meta_key);
                }
            }
        }

        // 画像を削除
        wp_delete_attachment($attachment_id, true);
    }

    return $file;
});

/**
 * 新しい画像がアップロードされたときに、以前使っていた投稿へ再設定する。
 *
 * @param int $new_attachment_id 新規アップロードされた attachment の ID
 * @return void
 */
add_action('add_attachment', function ($new_attachment_id) {
    $log_prefix = '[add_attachment]';

    $file_path = get_post_meta($new_attachment_id, '_wp_attached_file', true);
    if (!$file_path) {
        return;
    }

    $filename = basename($file_path);
    $old_ids = $GLOBALS['_replaced_attachment_ids'][$filename] ?? [];

    if (empty($old_ids)) {
        return;
    }

    foreach ($old_ids as $old_id) {
        // post_parent 再設定
        if (isset($GLOBALS['_parent_before_delete'][$old_id])) {
            $parent_id = $GLOBALS['_parent_before_delete'][$old_id];
            wp_update_post([
                'ID' => $new_attachment_id,
                'post_parent' => $parent_id,
            ]);
        }

        // メタキー再設定
        $meta_map = $GLOBALS['_image_meta_before_delete'][$old_id] ?? [];
        foreach ($meta_map as $meta_key => $post_ids) {
            foreach ($post_ids as $post_id) {
                update_post_meta($post_id, $meta_key, $new_attachment_id);
            }
        }
    }
});

今後に関して

共有会で出たテーマで、優先度の関係で未着手になっている作業もいくつかあります…
個人的に一番推し進めたいのは社内の開発チームが運営する「フロントエンド推進室」が社内公開してくれたGit上の自動校正ツール「textlint-rule-preset-optim」の導入です!
このツールは、オプティムの用語集やUXライティングのルールを反映し、「OPTiMらしい文書」をサポートするtextlintルールプリセットになっています。
プロモチームが管理するWebサイトの全リポジトリに導入して文章校正をぐんと楽にしたい🥺
効果が明らかなため、早く導入を進めたいです…!!!

今年度はこうして運用改善を進めてはいるものの、案件が忙しくなってきて改善の工数が思うように確保できない時もしばしば…
影響や重要度の高いものから順々に改善を重ね、より良いチーム作りができるようにしていきたいです。

まとめ

今まで一人で考えてきたことも、知識の共有とディスカッションを重ね、チームにとってどういう運用方法が最適なのか。
改善方法をチーム全体で確認し合うことで、一方的だった運用も相互理解が深まったと感じています。

Web制作チームの共有会のおさらい

  1. 開催頻度と時間を決める(週1・45分)
  2. 議題登録のルールを作る(お困りごと→各自持ち寄った情報共有)
  3. 改善ポイントをチケット化する
  4. やりきる…!!!💪

おわり

オプティムでは、エンジニアだけではなくプロモ・デザインUで一緒に働く仲間も探しています!
プロモ・デザインUでは、UI/UXデザインやブランディング、Web制作、マーケティングなど幅広い仕事をしています。
「Webやデザインが好き」「ブランディングに興味がある」「改善や工夫が楽しい!」という方、ぜひご応募お待ちしています。