ការអភិវឌ្ឍគេហទំព័រ Wordpress ជាមួយ Custom Theme និង ACF
1. Setup Environment
Setting up a consistent local environment ensures that what you build on your machine will work perfectly when you eventually push it to a live server.
1.1. Local Server Setup
We are using LocalWP because it simplifies the management of PHP versions, web servers (Nginx/Apache), and database access.
- Download & Install: Download the latest version of LocalWP from https://localwp.com.
- Create a New Site: * Open LocalWP and click the "+" button (bottom left).
- Select "Create a new site" and click Continue.
- Site Name: Enter your project name (e.g., my-custom-theme).
- Environment Settings:
- Choose Preferred for the easiest setup, or Custom if you need a specific PHP version (7.4 or 8.x are standard for modern development).
- WordPress Credentials:
- Set your Admin Username, Password, and Email. (Keep these handy!)
- Finalize: Click Add Site. LocalWP will provision the server and install WordPress for you.
1.2. Installing a Fresh WordPress
With LocalWP, the installation is handled during the site creation process. Once the site is "Running":
- Admin Dashboard: Click the "WP Admin" button in LocalWP to log in.
- Site View: Click "Open Site" to see the default front-end.
- SSL: In the LocalWP dashboard, under the "Site Setup" tab, click Trust next to the SSL certificate. This prevents "Your connection is not private" browser warnings.
1.3. WordPress File Structure
Understanding where your code lives is vital. Below is a high-level view of a standard WordPress directory. Your work will almost exclusively happen inside the themes folder.
| Directory/File | Description |
|---|---|
wp-admin/
|
Core WordPress dashboard files. Never edit. |
wp-admin/
|
Core WordPress logic and libraries. Never edit. |
wp-content/
|
The Storage for your site. This is where your custom work lives. |
↳ plugins/
|
Where ACF Pro and other functionality will be installed. |
wp-config.php
|
The core configuration file (DB credentials, security keys). |
1.4. Installing & Setup Custom Theme (_s)
Instead of building from scratch, we use Underscores as a "starter" theme. It provides the essential PHP structure so you don't have to reinvent the wheel.
- Generate the Theme:
- Go to Underscores.me.
- Theme Name: Your Custom Theme Name.
- Slug: my-custom-theme (lowercase, no spaces).
- Click Generate. A .zip file will download.
- Install the Theme:
- In LocalWP, click the arrow next to your site path (e.g., ~/Local Sites/my-project) to open the folder.
- Navigate to app/public/wp-content/themes/.
- Extract your _s zip file into this folder.
- Activate:
- Go to your WordPress Admin Dashboard.
- Navigate to Appearance > Themes.
- Locate your new theme and click Activate.
Note: Your site will look very plain at first. This is normal! Underscores is "naked" CSS-wise, giving you a clean slate to build your own design.
2. Theme Structure
2.1. Theme File Structure
Once you open your theme folder (found in wp-content/themes/your-theme-name ), you’ll see
several files. Here
is what they do:
2.2. Header & Menu
To make the header dynamic, we use specific WordPress functions so that changes in the Dashboard reflect on the site.
- The Custom Logo: To display the logo uploaded in Appearance > Customize, use:
<?php the_custom_logo() ?> - The Navigation Menu: Underscores already sets up a menu location. To output it, use:
wp_nav_menu( array( 'theme_location' => 'menu-1', 'menu_id' => 'primary-menu', ) );
2.3. Footer
The footer usually contains copyright info and the wp_footer() hook.
Important: Never remove <?php wp_footer(); ?>. It sits just before the closing
</body> tag and is where WordPress injects scripts and the Admin Bar.
2.4. Sidebar
If your design requires a sidebar, it is controlled via sidebar.php.
-
Check for active sidebar:
is_active_sidebar( 'sidebar-1' )checks if any widgets are actually placed there. -
Displaying it:
dynamic_sidebar( 'sidebar-1' );triggers the widgets you see in the WordPress Admin under Appearance > Widgets.
2.5. Custom CSS, JS & PHP
For a clean project, don't just dump everything into style.css. Organization is key.
Where to put files:
- Create a folder named
assetsin your theme root. - inside, create subfolders:
assets/css/andassets/js/. - Place your
custom-styles.cssandcustom-script.js there.
How to Enqueue (Connect) them: Open functions.php and look for the function ending in
_scripts. Add your files using wp_enqueue_style and
wp_enqueue_script:
function my_theme_scripts() {
// Enqueue Custom CSS
wp_enqueue_style( 'my-custom-style', get_template_directory_uri() . '/assets/css/custom-styles.css', array(), '1.0.0' );
// Enqueue Custom JS (The 'true' at the end puts it in the footer)
wp_enqueue_script( 'my-custom-js', get_template_directory_uri() . '/assets/js/custom-script.js', array('jquery'), '1.0.0', true );
}
add_action( 'wp_enqueue_scripts', 'my_theme_scripts' );
2.6. Specific Page Templates (Static naming)
Instead of selecting a template from a dropdown, WordPress allows you to target specific pages based on their slug or ID using its naming hierarchy. This is the preferred method for developers who want to keep the design locked into the code.
The "Front Page" (Home)
To create a unique design for your homepage that is different from your blog index:
- Create a file named
front-page.php. - WordPress will automatically use this file for whatever page you set as the "Static Front Page" in Settings > Reading.
Targeting Specific Pages by Slug
If you have a page in the dashboard with the URL slug contact or about-us, you can create dedicated files for them:
page-about-us.phppage-contact.php
How to Structure the Code
Since we are avoiding the Gutenberg/Block editor, your code in these files will typically look like this:
<?php get_header(); ?>
<main id="primary" class="site-main">
<?php
while ( have_posts() ) :
the_post();
// 1. Static HTML/CSS structure goes here
?>
<section class="hero-banner">
<h1><?php the_title(); ?></h1>
</section>
<section class="custom-content">
<?php the_content(); // Pulls from the editor if needed ?>
</section>
<?php
endwhile;
?>
</main>
<?php get_footer(); ?>
Why use this method?
- Design Integrity: Clients cannot accidentally break the layout by moving blocks around in the editor.
- Performance: You only load the CSS and JS needed for that specific page.
- Clean Code: You have full control over every
<div>and class name, making it easier to integrate custom Javascript libraries (like GSAP or Swiper.js).
3. Advanced Custom Field
3.1. What is ACF?
ACF allows you to add "Field Groups" to specific pages or post types. Instead of typing everything into a big text box, you create structured data (e.g., a field specifically for a "Price" or a "Hero Image").
The Core Rule:
- To get a field:
get_field('field_name'); - To display a field:
the_field('field_name');
3.2. Text & Text area
- Text: Best for headlines or short strings.
- Text Area: Best for short descriptions.
- Tip: Use the "New Lines" setting to "Automatically add
<br>" if you want to preserve line breaks without using a full editor.
- Tip: Use the "New Lines" setting to "Automatically add
3.3. Image
When setting up an Image field, always set the Return Format to Image Array. This gives you access to the URL, Alt text, and specific sizes.
<?php
$image = get_field('hero_image');
if( !empty( $image ) ): ?>
<img src="<?php echo esc_url($image['url']); ?>" alt="<?php echo esc_attr($image['alt']); ?>" />
<?php endif; ?>
3.4. Wysiwyg Editor
The "What You See Is What You Get" editor. Use this for the main body of content where the user might need to bold text or add lists.
3.5. Icon Picker
Note: ACF does not have a native icon picker. You usually use a plugin like "ACF Font Awesome" or a "Select" field where the values are CSS class names.
Implementation: Store the class name (e.g., fa-star) and output it:
<i class="fa <?php the_field('icon_class'); ?>"></i>.
3.6. Repeater (Pro Only)
The most powerful field. It allows you to create a set of sub-fields that can be repeated infinitely (e.g., a "Team Members" list).
<?php if( have_rows('team_members') ): ?>
<div class="team-grid">
<?php while( have_rows('team_members') ): the_row(); ?>
<div class="member">
<h3><?php the_sub_field('name'); ?></h3>
<p><?php the_sub_field('role'); ?></p>
</div>
<?php endwhile; ?>
</div>
<?php endif; ?>
3.7. Gallery
Similar to the Image field but returns an array of multiple images. Perfect for sliders or portfolios. Use a
foreach loop to display them.
3.8. oEmbed
Allows users to paste a YouTube, Vimeo, or Twitter link, and WordPress automatically converts it into an iframe.
<div class="video-container">
<?php the_field('video_link'); ?>
</div>
3.9. Email, Number & URL
These fields have built-in validation.
- Email: Ensures a valid
@format. - URL: Ensures
http://is present. - Number: Useful for data you might want to calculate or use in scripts.
3.10. Tab & Accordion
These are Layout Fields. They don't store data themselves; they organize your other fields in the WordPress Admin so the user isn't overwhelmed by a giant list.
- Tabs: Good for separating "Header Settings," "Content," and "Footer Settings" within the same page editor.
3.11. Global Elements (Options Page)
Sometimes you need a field that appears on every page (like a phone number in the footer or social media links). For this, we create an Options Page.
- Register it in
functions.php:if( function_exists('acf_add_options_page') ) { acf_add_options_page(array( 'page_title' => 'Global Settings', 'menu_title' => 'Global Settings', 'menu_slug' => 'global-settings', 'capability' => 'edit_posts', 'redirect' => false )); } - Retrieve the data: You must add
'options'as the second parameter:the_field('phone_number', 'options');
4. Post Type & Custom Post Type
4.1. Post (Blog)
By default, WordPress comes with "Posts." These are designed for chronological content and come pre-loaded
with Categories and Tags. In a custom theme, these usually power your "News" or
"Blog" section.
4.2. Custom Post Type (CPT)
Sometimes "Posts" aren't enough. If you are building a real estate site, you need a "Properties" post type. If it's a portfolio, you need "Projects."
To register a CPT, add this to your functions.php. (While plugins like CPT UI exist, coding it
keeps your theme lightweight):
function register_custom_post_types() {
register_post_type('projects', array(
'labels' => array(
'name' => 'Projects',
'singular_name' => 'Project',
),
'public' => true,
'has_archive' => true,
'supports' => array('title', 'editor', 'thumbnail'),
'menu_icon' => 'dashicons-portfolio', // Icon in sidebar
'rewrite' => array('slug' => 'projects'),
));
}
add_action('init', 'register_custom_post_types');
4.3. Post Query (The Loop)
To display these posts on a page (like a "Latest Projects" section on your Home Page), we use
WP_Query. This is the most important tool in your kit.
$args = array(
'post_type' => 'projects',
'posts_per_page' => 3,
'orderby' => 'date',
'order' => 'DESC',
);
$project_query = new WP_Query($args);
if ($project_query->have_posts()) :
while ($project_query->have_posts()) : $project_query->the_post();
// Your HTML & ACF fields go here
the_title('<h3>', '</h3>');
the_post_thumbnail('medium');
endwhile;
wp_reset_postdata(); // Essential! Restores the global $post variable
endif;
4.4. Archive Page
An Archive is the automated list view of a post type (e.g., yourdomain.com/projects/).
- Filename:
archive-projects.php - WordPress automatically recognizes this file for your CPT. It uses a standard "Loop" without needing a
custom
WP_Query.
4.5. Single Page
The Single template is the layout for an individual post (e.g., the specific project page).
- Filename:
single-projects.php - This is where you usually go heavy with ACF. You’ll use
get_field()to pull in specific project details like "Client Name," "Year," or "Project Gallery."
4.6. Filters & Search
To let users find specific content, you need to interact with the WordPress Search engine.
- Search Form: Create a
searchform.phpto customize the look of your search bar. - Results Page: Use
search.phpto display the results. - The "Secret" Filter: If you want to filter by ACF fields (e.g., "Show only projects where 'Service' is 'Design'"), you modify the query using the
meta_queryargument in yourWP_Query.
Pro Tip: For advanced filtering (AJAX filters where the page doesn't refresh), most developers use the "Search & Filter Pro" plugin or custom JavaScript fetching.
5. Taxonomies
5.1. What is a Taxonomy?
In WordPress, "Taxonomy" is a fancy word for a grouping mechanism.
- Default Taxonomies: Categories (hierarchical) and Tags (flat).
- Custom Taxonomies: These allow you to create specific labels for your Custom Post Types. For a "Books" post type, you might create a "Genre" or "Author" taxonomy.
5.2. Registering a Custom Taxonomy
Like CPTs, you register these in functions.php. You must link the taxonomy to a specific post
type during registration.
function register_custom_taxonomies() {
register_taxonomy('project_type', 'projects', array(
'labels' => array(
'name' => 'Project Types',
'singular_name' => 'Project Type',
),
'hierarchical' => true, // True = like Categories, False = like Tags
'show_admin_column' => true,
'rewrite' => array('slug' => 'project-type'),
));
}
add_action('init', 'register_custom_taxonomies');
5.3. Taxonomy Archive (taxonomy-{taxonomy}.php)
When a user clicks on a "Web Design" category link, WordPress looks for a specific file to display all projects in that category.
- Filename:
taxonomy-project_type.php - If that file doesn't exist, it falls back to
archive.php, and eventuallyindex.php.
5.4. Displaying Terms on a Single Post
To show which "Project Types" are assigned to a specific project on the single-projects.php
page, use the get_the_terms() function.
<?php
$terms = get_the_terms( get_the_ID(), 'project_type' );
if ( $terms && ! is_wp_error( $terms ) ) :
foreach ( $terms as $term ) {
echo '<span class="term-badge">' . esc_html( $term->name ) . '</span>';
}
endif;
?>
5.5. Querying by Taxonomy (tax_query)
If you want to create a "Related Projects" section that only shows projects from the same "Project Type,"
you use a tax_query inside your WP_Query arguments.
$args = array(
'post_type' => 'projects',
'tax_query' => array(
array(
'taxonomy' => 'project_type',
'field' => 'slug',
'terms' => 'web-design', // Show only Web Design projects
),
),
);
$query = new WP_Query( $args );
6. Contact Form (CF7)
Contact Form 7 is the industry standard for custom themes because it stays out of your way and gives you raw HTML to style.
6.1. Why CF7 for Custom Themes?
Unlike "drag-and-drop" builders, CF7 allows you to write your own markup. This ensures the form fields match your theme’s CSS classes perfectly.
6.2. Structuring the Form Markup
In the CF7 editor, you can wrap your inputs in custom <div> containers to match your grid
system:
<div class="form-row">
<div class="column">[text* your-name placeholder "Name"]</div>
<div class="column">[email* your-email placeholder "Email"]</div>
</div>
<div class="form-row">
[textarea your-message placeholder "Message"]
</div>
[submit "Send Message"]
6.3. Displaying the Form in PHP
Once you create a form, CF7 provides a shortcode. To keep it dynamic, you can use an ACF URL or Text field for the shortcode, or hardcode it into your template:
<section class="contact-section">
<h2>Get in Touch</h2>
<?php echo do_shortcode('[contact-form-7 id="123" title="Contact form 1"]'); ?>
</section>
6.4. Styling CF7
CF7 forms are notorious for looking like 1995 HTML out of the box. Use your assets/css/custom-styles.css to target them:
.wpcf7-form input[type="text"],
.wpcf7-form input[type="email"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.wpcf7-submit {
background-color: #0073aa;
color: #fff;
cursor: pointer;
}