Every time I start a new client project, I work from the same base theme structure. Not a starter theme from GitHub. Not Underscores. A structure I’ve refined over two years of building real sites for real clients.
Here’s the breakdown.
File structure
Root level: functions.php, style.css (just the theme header), header.php, footer.php, front-page.php, and page templates as needed. Nothing goes in the root that doesn’t need to be there.
/inc/ folder: All PHP logic lives here. Separated by responsibility. enqueue.php for scripts and styles. cpts.php for custom post types and taxonomies. seo.php for meta tags and structured data. contact.php for form handling. Clean separation, easy to find anything.
/assets/css/: One main CSS file. No build step. No Sass. No PostCSS. Just CSS custom properties and good organization. Design tokens at the top, components in the middle, responsive at the bottom.
/assets/js/: One main JS file. Vanilla JS. No npm, no Webpack, no Vite. The whole thing is usually under 10KB.
Why no build tools
Because the client shouldn’t need to run npm install to make a text change on their website. Every time I’ve used a build step in a client project, it’s become a support burden within a year. Dependencies go stale. Node versions conflict. The agency that takes over the project after me can’t figure out the toolchain.
Plain PHP, CSS, and JS don’t have this problem. They work today and they’ll work in five years.
Custom post types over ACF
I register CPTs and meta boxes directly in cpts.php. No ACF dependency. The admin UI is custom but intentional. Clients get exactly the fields they need, organized the way that makes sense for their content, without 40 ACF field groups cluttering the database.
Is this more work upfront? Slightly. But the result is a theme that has zero plugin dependencies for its core functionality. The client can update WordPress, update PHP, update their hosting, and nothing breaks because there’s nothing to break.
The philosophy
Every dependency is a liability. Every abstraction is a future support ticket. Every build step is a barrier between the client and their content.
I write code that the next developer can read without documentation. That’s the entire strategy.