カスタマイズ 2023.11.16 2024.06.27
WordPressの投稿一覧ページをカスタマイズしてみる【前編 〜edit.php解説〜】
目次
はじめに
WordPressの管理画面の投稿一覧で絞り込みできる項目を増やしたり、検索機能でタイトル以外でも検索できるようにしたりといった、一覧ページに便利な機能を追加したいと思ったことはありませんか?
今回は、管理画面の一覧ページをカスタマイズするための前提として、投稿一覧ページを表示しているedit.phpの中身を紹介し、どのようにページが表示されているのか説明したいと思います。
edit.php とは
edit.phpは、WordPress管理画面内の「投稿一覧」、「固定ページ一覧」といった一覧ページを表示する php ファイルです。
「投稿一覧」では「https://〇〇〇.com/wp-admin/edit.php」、
「固定ページ一覧」では「https://〇〇〇.com/wp-admin/edit.php?post_type=page」
というリンクで表示されています。
また、カスタム投稿タイプを作成した場合、「https://〇〇〇.com/wp-admin/edit.php?post_type=xxx(カスタム投稿タイプ名)」
といったように呼び出されます。
例として、functions.phpにregister_post_typeを記述して、カスタム投稿タイプを作成してみます。[^1]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function create_custom_post_type() { $labels = array( 'name' => 'カスタム投稿', 'menu_name' => 'カスタム投稿', ); $args = array( 'labels' => $labels, 'public' => true, 'show_ui' => true, 'show_in_nav_menus' => true, 'show_in_menu' => true, 'show_in_admin_bar' => true, 'menu_icon' => 'dashicons-media-code', ); register_post_type('custom', $args); } add_action('init', create_custom_post_type(...)); |
すると、メニューバーに「カスタム投稿」というメニューが表示されました。クリックして移動してみましょう。
カスタム投稿一覧ページのURLを見ると、「/wp-admin/edit.php?post_type=custom」となっています。
register_post_typeの第 1 引数( = カスタム投稿タイプ名)をパラメータとして edit.php が呼び出されていることがわかります。
edit.php がどのページを表示するファイルかわかりました。
次にedit.phpのコードを確認していきます。
重要な部分である「どの投稿タイプを表示するか」「どのように一覧を表示しているか」を決定している場所を見ていきましょう。
どの投稿タイプを表示するか
ページを表示するかの判定はグローバル変数『$typenow』と『$post_type』、『$post_type_object』を使用して判定しています。
それぞれ説明します。
$typenow を使用した文 [^edit.php line15-29]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
global $typenow; if ( ! $typenow ) { wp_die( __( 'Invalid post type.' ) ); } if ( ! in_array( $typenow, get_post_types( array( 'show_ui' => true ) ), true ) ) { wp_die( __( 'Sorry, you are not allowed to edit posts in this post type.' ) ); } if ( 'attachment' === $typenow ) { if ( wp_redirect( admin_url( 'upload.php' ) ) ) { exit; } } |
$typenowは現在のページの投稿タイプを示すグローバル変数です。
投稿であれば『post』、固定ページであれば『page』というように、投稿タイプの名前が入っています。
そして、$typenow が空文字だったとき、『wp_die』関数を実行するようになっています。
『wp_die』は、WordPress の実行を終了し、エラーメッセージが入った html ページを表示する関数です。
引数として入れた文字列がエラーメッセージとして表示されます。
試しに、先ほどの『custom』のカスタム投稿タイプの一覧ページを表示した状態で、functions.phpに記述した『register_post_type』 のスラッグ名を書き換えてみます。
1 |
register_post_type('error', $args); |
これで、『custom』という名前のカスタム投稿タイプは無くなりました。
『custom』のカスタム投稿タイプの一覧ページをリロードすると、このようなページが表示されます。
『custom』という名前のカスタム投稿タイプを表示するページなのに、『custom』という名前の投稿タイプが存在しなくなったため、エラーページが表示されました。
21 行目の if 文も同様に、カスタム投稿タイプのステータス『show_ui』が『true/false』を判定し、『false』であればエラーページを表示するというものとなります。
『show_ui』は管理画面のメニューバーに投稿タイプを表示するかを決定する値です。
『false』だと、メニューバーに投稿タイプのメニューが表示されません。
25 行目の if 文は、投稿タイプが『attachment』であれば、upload.php にリダイレクトするというようになっています。
upload.php は、メディアのライブラリページのファイルです。
残りも見ていきましょう。
$post_type、$post_type_object を使用した文 [^edit.php line35-50]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
global $post_type, $post_type_object; $post_type = $typenow; $post_type_object = get_post_type_object( $post_type ); if ( ! $post_type_object ) { wp_die( __( 'Invalid post type.' ) ); } if ( ! current_user_can( $post_type_object->cap->edit_posts ) ) { wp_die( '<h1>' . __( 'You need a higher level of permission.' ) . '</h1>' . '<p>' . __( 'Sorry, you are not allowed to edit posts in this post type.' ) . '</p>', 403 ); } |
『$post_type』に『$typenow』、つまり投稿タイプの名前を代入し、『get_post_type_object』関数の返り値を『$post_type_object』に代入しています。
『get_post_type_object』は、指定した投稿タイプのオブジェクトを取得する関数です。
オブジェクトには、『register_post_type』の引数にもあった『labels』、『show_ui』、『show_in_menu』といった値が入っています。
最初の if 文は、この関数で何もオブジェクトを取得出来なかったとき、エラーページを表示するようになっています。
2つめの if 文は、『current_user_can』関数で、ユーザーがこのページの投稿タイプの編集権限を持っているかどうかを取得し、持っていなければエラーページを返すようになっています。
一覧の表示
次に、一覧ページがどのように作られているか見ていきます。 具体的には、下の図の部分です。
一覧を表示する際には、『$wp_list_table』という変数が使われています。『$wp_list_table』について見ていきましょう。
$wp_list_table
52 行目
1 |
$wp_list_table = _get_list_table( 'WP_Posts_List_Table' ); |
ここで『$wp_list_table』を宣言しています。
『_get_list_table』は『WP_List_Table 』[^関数リファレンス/WP List Table]クラスのインスタンスをフェッチする関数です。
この場合は、引数に入れた『WP_Posts_List_Table』クラスのインスタンスがフェッチされます。
235 行目
1 |
$wp_list_table->prepare_items(); |
『prepare_items』メソッドは、表示するデータを準備する関数で、描画の前にはこのメソッドを使用する必要があります。
HTML 部分 [^2]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?php $wp_list_table->views(); ?> <form id="posts-filter" method="get"> <?php $wp_list_table->search_box( $post_type_object->labels->search_items, 'post' ); ?> <input type="hidden" name="post_status" class="post_status_page" value="<?php echo ! empty( $_REQUEST['post_status'] ) ? esc_attr( $_REQUEST['post_status'] ) : 'all'; ?>" /> <input type="hidden" name="post_type" class="post_type_page" value="<?php echo $post_type; ?>" /> <?php if ( ! empty( $_REQUEST['author'] ) ) { ?> <input type="hidden" name="author" value="<?php echo esc_attr( $_REQUEST['author'] ); ?>" /> <?php } ?> <?php if ( ! empty( $_REQUEST['show_sticky'] ) ) { ?> <input type="hidden" name="show_sticky" value="1" /> <?php } ?> <?php $wp_list_table->display(); ?> </form> |
HTML 部分はとてもシンプルなコードですが、『$wp_list_table』が使われている場所が 3 箇所あります。この 3 か所は以下の図の場所を描画しています。
『『views()』、『『search_box()』、『『display()』についてそれぞれ見ていきましょう。
views() [^3]
『views()』は、使用可能なビューのリストを描画するメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public function views() { $views = $this->get_views(); $views = apply_filters( "views_{$this->screen->id}", $views ); if ( empty( $views ) ) { return; } $this->screen->render_screen_reader_content( 'heading_views' ); echo "<ul class='subsubsub'>\n"; foreach ( $views as $class => $view ) { $views[ $class ] = "\t<li class='$class'>$view"; } echo implode( " |</li>\n", $views ) . "</li>\n"; echo '</ul>'; } |
簡単に説明すると、「すべて」、「ゴミ箱」といったリストのリンクなどを含んだ HTML 要素が入った配列を返す『get_views』メソッドを呼び出し、取得した配列を『foreach』で回してリストを作成しています。
search_box() [^4]
search_box()は、検索ボックスを描画するメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<?php public function search_box( $text, $input_id ) { if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) { return; } $input_id = $input_id . '-search-input'; if ( ! empty( $_REQUEST['orderby'] ) ) { echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />'; } if ( ! empty( $_REQUEST['order'] ) ) { echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />'; } if ( ! empty( $_REQUEST['post_mime_type'] ) ) { echo '<input type="hidden" name="post_mime_type" value="' . esc_attr( $_REQUEST['post_mime_type'] ) . '" />'; } if ( ! empty( $_REQUEST['detached'] ) ) { echo '<input type="hidden" name="detached" value="' . esc_attr( $_REQUEST['detached'] ) . '" />'; } ?> <p class="search-box"> <label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo $text; ?>:</label> <input type="search" id="<?php echo esc_attr( $input_id ); ?>" name="s" value="<?php _admin_search_query(); ?>" /> <?php submit_button( $text, '', '', false, array( 'id' => 'search-submit' ) ); ?> </p> <?php } |
引数を 2 つ持ち、第 1 引数はボタンの文字、第 2 引数は検索する『input』タグの id になります。
display() [^5]
『display()』は、テーブルを描画するメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<?php public function display() { $singular = $this->_args['singular']; $this->display_tablenav( 'top' ); $this->screen->render_screen_reader_content( 'heading_list' ); ?> <table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>"> <?php $this->print_table_description(); ?> <thead> <tr> <?php $this->print_column_headers(); ?> </tr> </thead> <tbody id="the-list" <?php if ( $singular ) { echo " data-wp-lists='list:$singular'"; } ?> > <?php $this->display_rows_or_placeholder(); ?> </tbody> <tfoot> <tr> <?php $this->print_column_headers( false ); ?> </tr> </tfoot> </table> <?php $this->display_tablenav( 'bottom' ); } |
display()メソッドの中で複数のメソッドが呼び出されています。その中で、実際に表示されている部分を描画しているメソッドを説明します。
display_tablenav() [^6]
『>display_tablenav()』は、プルダウンリストを描画するメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?php protected function display_tablenav( $which ) { if ( 'top' === $which ) { wp_nonce_field( 'bulk-' . $this->_args['plural'] ); } ?> <div class="tablenav <?php echo esc_attr( $which ); ?>"> <?php if ( $this->has_items() ) : ?> <div class="alignleft actions bulkactions"> <?php $this->bulk_actions( $which ); ?> </div> <?php endif; $this->extra_tablenav( $which ); $this->pagination( $which ); ?> <br class="clear" /> </div> <?php } |
このメソッドの中の、『bulk_actions()』メソッドでプルダウンリストを作成しています。 『extra_tablenav()』は、複数のプルダウンリストを作成するときに使用します。詳細は次回説明します。
bulk_actions() [^7]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
protected function bulk_actions( $which = '' ) { if ( is_null( $this->_actions ) ) { $this->_actions = $this->get_bulk_actions(); $this->_actions = apply_filters( "bulk_actions-{$this->screen->id}", $this->_actions ); $two = ''; } else { $two = '2'; } if ( empty( $this->_actions ) ) { return; } echo '<label for="bulk-action-selector-' . esc_attr( $which ) . '" class="screen-reader-text">' . __( 'Select bulk action' ) . '</label>'; echo '<select name="action' . $two . '" id="bulk-action-selector-' . esc_attr( $which ) . "\">\n"; echo '<option value="-1">' . __( 'Bulk actions' ) . "</option>\n"; foreach ( $this->_actions as $key => $value ) { if ( is_array( $value ) ) { echo "\t" . '<optgroup label="' . esc_attr( $key ) . '">' . "\n"; foreach ( $value as $name => $title ) { $class = ( 'edit' === $name ) ? ' class="hide-if-no-js"' : ''; echo "\t\t" . '<option value="' . esc_attr( $name ) . '"' . $class . '>' . $title . "</option>\n"; } echo "\t" . "</optgroup>\n"; } else { $class = ( 'edit' === $key ) ? ' class="hide-if-no-js"' : ''; echo "\t" . '<option value="' . esc_attr( $key ) . '"' . $class . '>' . $value . "</option>\n"; } } echo "</select>\n"; submit_button( __( 'Apply' ), 'action', '', false, array( 'id' => "doaction$two" ) ); echo "\n"; } |
『get_bulk_actions()』メソッドは『[“edit” => “編集”, “trash” => “ゴミ箱へ移動”]』のような連想配列を返します。 この連想配列を『_actions』プロパティに入れ、それを『foreach』で回すことによってプルダウンリストを生成しています。
print_column_headers() [^8]
『print_column_headers()』メソッドは、『<th>』部分を描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
public function print_column_headers( $with_id = true ) { list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); $current_url = remove_query_arg( 'paged', $current_url ); if ( isset( $_GET['orderby'] ) ) { $current_orderby = $_GET['orderby']; } else { $current_orderby = ''; } if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) { $current_order = 'desc'; } else { $current_order = 'asc'; } if ( ! empty( $columns['cb'] ) ) { static $cb_counter = 1; $columns['cb'] = '<label class="label-covers-full-cell" for="cb-select-all-' . $cb_counter . '">' . '<span class="screen-reader-text">' . __( 'Select All' ) . '</span>' . '</label>' . '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />'; $cb_counter++; } foreach ( $columns as $column_key => $column_display_name ) { $class = array( 'manage-column', "column-$column_key" ); $aria_sort_attr = ''; $abbr_attr = ''; $order_text = ''; if ( in_array( $column_key, $hidden, true ) ) { $class[] = 'hidden'; } if ( 'cb' === $column_key ) { $class[] = 'check-column'; } elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ), true ) ) { $class[] = 'num'; } if ( $column_key === $primary ) { $class[] = 'column-primary'; } if ( isset( $sortable[ $column_key ] ) ) { $orderby = isset( $sortable[ $column_key ][0] ) ? $sortable[ $column_key ][0] : ''; $desc_first = isset( $sortable[ $column_key ][1] ) ? $sortable[ $column_key ][1] : false; $abbr = isset( $sortable[ $column_key ][2] ) ? $sortable[ $column_key ][2] : ''; $orderby_text = isset( $sortable[ $column_key ][3] ) ? $sortable[ $column_key ][3] : ''; $initial_order = isset( $sortable[ $column_key ][4] ) ? $sortable[ $column_key ][4] : ''; if ( '' === $current_orderby && $initial_order ) { $current_orderby = $orderby; $current_order = $initial_order; } if ( $current_orderby === $orderby ) { if ( 'asc' === $current_order ) { $order = 'desc'; $aria_sort_attr = ' aria-sort="ascending"'; } else { $order = 'asc'; $aria_sort_attr = ' aria-sort="descending"'; } $class[] = 'sorted'; $class[] = $current_order; } else { $order = strtolower( $desc_first ); if ( ! in_array( $order, array( 'desc', 'asc' ), true ) ) { $order = $desc_first ? 'desc' : 'asc'; } $class[] = 'sortable'; $class[] = 'desc' === $order ? 'asc' : 'desc'; $asc_text = __( 'Sort ascending.' ); $desc_text = __( 'Sort descending.' ); $order_text = 'asc' === $order ? $asc_text : $desc_text; } if ( '' !== $order_text ) { $order_text = ' <span class="screen-reader-text">' . $order_text . '</span>'; } $abbr_attr = $abbr ? ' abbr="' . esc_attr( $abbr ) . '"' : ''; $column_display_name = sprintf( '<a href="%1$s">' . '<span>%2$s</span>' . '<span class="sorting-indicators">' . '<span class="sorting-indicator asc" aria-hidden="true"></span>' . '<span class="sorting-indicator desc" aria-hidden="true"></span>' . '</span>' . '%3$s' . '</a>', esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ), $column_display_name, $order_text ); } $tag = ( 'cb' === $column_key ) ? 'td' : 'th'; $scope = ( 'th' === $tag ) ? 'scope="col"' : ''; $id = $with_id ? "id='$column_key'" : ''; if ( ! empty( $class ) ) { $class = "class='" . implode( ' ', $class ) . "'"; } echo "<$tag $scope $id $class $aria_sort_attr $abbr_attr>$column_display_name</$tag>"; } } |
簡単に説明すると、『get_column_info』メソッドで『$_column_headers』というプロパティをセットします。
『$_column_headers』は『$columns』、『$hidden』、『$sortable』、『$primary』という 4 つの値が入っています。
『$columns』はカラム全体の配列、『$hidden』は hidden フィールドの値からなる配列、『$sortable』はソート可能なカラムの配列、『$primary』はカラムのスラッグとタイトルです。
そして『$colums』を『foreach』で回して『<th>』要素を生成しています。
display_rows_or_placeholder() [^9]
『display_rows_or_placeholder()』メソッドは、投稿のテーブルを描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
public function display_rows_or_placeholder() { if ( $this->has_items() ) { $this->display_rows(); } else { echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">'; $this->no_items(); echo '</td></tr>'; } } public function display_rows() { foreach ( $this->items as $item ) { $this->single_row( $item ); } } public function single_row( $item ) { echo '<tr>'; $this->single_row_columns( $item ); echo '</tr>'; } protected function single_row_columns( $item ) { list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); foreach ( $columns as $column_name => $column_display_name ) { $classes = "$column_name column-$column_name"; if ( $primary === $column_name ) { $classes .= ' has-row-actions column-primary'; } if ( in_array( $column_name, $hidden, true ) ) { $classes .= ' hidden'; } $data = 'data-colname="' . esc_attr( wp_strip_all_tags( $column_display_name ) ) . '"'; $attributes = "class='$classes' $data"; if ( 'cb' === $column_name ) { echo '<th scope="row" class="check-column">'; echo $this->column_cb( $item ); echo '</th>'; } elseif ( method_exists( $this, '_column_' . $column_name ) ) { echo call_user_func( array( $this, '_column_' . $column_name ), $item, $classes, $data, $primary ); } elseif ( method_exists( $this, 'column_' . $column_name ) ) { echo "<td $attributes>"; echo call_user_func( array( $this, 'column_' . $column_name ), $item ); echo $this->handle_row_actions( $item, $column_name, $primary ); echo '</td>'; } else { echo "<td $attributes>"; echo $this->column_default( $item, $column_name ); echo $this->handle_row_actions( $item, $column_name, $primary ); echo '</td>'; } } } |
ここでの重要な部分は『single_row_columns()』メソッドです。
ここで『get_column_headers()』メソッドのときと同様に『get_column_info()』メソッドでカラムの情報を取得して、『columns』を『foreach』で回して生成しています。
そして、『column_$column_name()』というメソッドがあれば、そのメソッドを使用して中身を描画します。無ければ、『column_default()』メソッドで中身を描画します。
『WP_List_Table』クラスでは、左のチェックボックス部分の作成に、『column_cb()』メソッドが使われています。
また、投稿一覧では、『column_title()』や『column_comments()』なども使われています。
詳しくは次回説明しますが、自分で『column_$column_name()』を定義すれば、特定のカラムの表示内容をカスタマイズすることもできます。
まとめと次回予告
edit.php の中身についてまとめると、
- どの投稿タイプのページなのかはグローバル変数$typenowの値を取得して判定している。取得できなければエラーページを表示する。
- 一覧ページの表示にはWP_List_Tableクラスを使用している。 views()メソッドで使用可能なビューのリスト、search_box()メソッドで検索ボックス、displayメソッドでプルダウンリストを含めたテーブル全体を描画している。この 3 つのメソッドの中でも他のメソッドが複数呼ばれている。
edit.php がどのようにページを表示しているかはわかったので、次回は一覧ページをカスタマイズしていきます。
参考記事
- 関数リファレンス/register post type(※参考にしたWordPress日本語版Codexサイトが消えたためリンク解除)
- 投稿一覧ページ(※参考にしたWordPress日本語版Codexサイトが消えたためリンク解除)
- WP_List_Table::views()
- WP_List_Table::search_box()
- WP_List_Table::display()
- WP_List_Table::display_tablenav()
- WP_List_Table::bulk_actions()
- WP_List_Table::print_column_headers()
- WP_List_Table::display_or_placeholder()
WordPressサイト制作・カスタマイズなら「wp.make」にお任せ!
WordPressでのサイト制作やリニューアルを検討する時、以下のようなお悩みはありませんか?
- WordPressに詳しい制作会社に依頼したいが、どこがいいかわからない…
- セキュリティ対策をしっかりしたいが、社内にノウハウがないのでプロに任せたい…
- WordPressに最適なサーバーの選定や構築から依頼したい…
- SEO対策や高速化も考慮したサイト構築を行なってほしい…
- 制作後の保守・運用についてもサポートしてほしい…
- 今のサイトを簡単に運用できるようにしてほしい…
「wp.make」は、WordPressのプロフェッショナル集団によるWordPressサイト制作・カスタマイズサービスです。
サイトの制作だけでなく、WordPressに最適なサーバーの選定や構築といったインフラ面の支援から、SEO対策や表示スピードの高速化、高度なセキュリティ対策や制作後の保守・運用サポートまで、WordPressに関わることならあらゆるお悩みを解消いたします。
既存のお客さまからも
「コミュニケーションが取りやすく、クオリティが高い」
「WordPressのプロとして信頼感がある」
と大変ご好評をいただいています。
WordPressサイトの制作・カスタマイズをご検討されているなら、ぜひ以下からお気軽にご相談ください。
WordPress開発・カスタマイズなら 『wp.make』
全案件WordPressのみ!
株式会社e2eの『wp.make』はWordPress専門のWeb制作サービスです。
WordPress案件だけを扱っているから、技術・ノウハウ・対応力が圧倒的!
【WordPressサイト制作でよくあるお悩み】
・運用シーンが想定されておらず、更新しづらかった…
・打ち合わせで専門用語が多くてわかりづらい…
・制作後の保守には対応してくれなかった…
こんな事態になる前に、ぜひ一度、ご相談ください!
WordPressサイトを作るなら、一番WordPressに詳しいところへ!