A WordPress theme technically needs two files. In practice, the difference between a theme you can still work on in two years and one you quietly rebuild is the structure you set up in the first hour. This is the starting structure we use, and the reasoning behind each decision.
The theme running this site was built this way, so you can read the result while you read the explanation. Open the page source and you will see what the choices below produce.
The two files WordPress insists on
style.css and index.php. The stylesheet needs a header comment declaring the theme name, and WordPress reads that comment to list the theme. That is genuinely all that is required for a theme to activate.
Almost nobody stops there, and the reason is the template hierarchy. WordPress looks for a specific file for each kind of request and falls back through a defined order until it finds one. Give it single.php and posts render with it. Give it nothing and everything falls through to index.php, which then has to handle every case with conditionals. That file becomes unreadable within a few months.
The folder structure
Here is the layout we start every theme with.
style.cssfor the theme header only, not the actual stylesfunctions.phpas a loader, almost no logic in itindex.php,front-page.php,home.php,single.php,page.php,archive.php,404.phpheader.phpandfooter.phpinc/for PHP that is not a template: setup, metadata, queries, anything with logictemplate-parts/for reusable fragments pulled in withget_template_part()page-templates/for alternative page layouts the editor can selectassets/css/,assets/js/,assets/fonts/
Two of those deserve explaining.
Keep functions.php as a table of contents
The most common failure in custom themes is a functions.php that grows to two thousand lines because every new requirement got appended to the bottom. Nobody can find anything, two features end up hooking the same filter, and changing one breaks the other.
Treat it as a loader. Define a constant for the theme directory, then require one file per concern from inc/. On this site those files separate theme setup and asset loading, page metadata and canonical handling, the related-posts query used under articles, the file that controls what crawlers are told, and a small file that removes the built-in search entirely.
That last one is a good example of why the structure pays off. Turning off WordPress search meant changing one self-contained file and deleting two templates. In a monolithic functions.php it would have meant surgery on a file that does ten other things.
Use style.css for the header, not the styles
WordPress needs the header comment in style.css, but nothing says your actual CSS has to live there. Put it in assets/css/main.css and enqueue that file. You get a sensible path, your editor stops loading a file with a comment block at the top, and cache busting is easier to manage.
Enqueue properly with wp_enqueue_style and wp_enqueue_script rather than writing tags into the header. Version your assets using the file modification time so a deploy invalidates the cache without you remembering to bump a number.
Self-host your fonts
Loading fonts from a third-party service costs a DNS lookup, a connection and a round trip before any text can render in the right typeface. Downloading the woff2 files into assets/fonts/ and declaring them in your own CSS removes all of that, and it removes a third party from your visitors’ request chain.
Subset to the character ranges you actually need, use font-display: swap, and preload only the one or two faces that appear above the fold. This is usually the single largest easy win on a marketing site, and it connects to the wider argument in performance budgets for marketing websites.
Template parts, and the trap in page templates
Anything that appears in more than one template becomes a template part. A post card, an author byline, an empty-results message. get_template_part() pulls it in, and you fix it once.
One warning worth having in advance. If you build a full-width page template that does not call the_title(), every page using it will render without an H1 unless the content supplies one. We hit exactly this on a page here: it looked fine, validated fine, and had no heading at all until we injected one into the content. If you write a template that omits the title, write it down somewhere the next person will read.
What belongs in a plugin instead
This is the line most custom themes get wrong, and it is worth drawing carefully.
A theme controls presentation. A plugin controls functionality that must survive a theme change. Custom post types, custom taxonomies, shortcodes, anything that creates content or data belongs in a plugin. If you register a custom post type in your theme and later switch themes, the content is still in the database but nothing displays it, and the client thinks you deleted it.
Metadata is the grey area. Fields that only exist to control how a theme renders something are defensible in the theme. Fields that hold real business data are not.
Escaping, and why it is not optional
Escape on output, every time. esc_html() for text, esc_attr() for attribute values, esc_url() for links, wp_kses_post() when you genuinely need to allow some markup through.
The argument that a value is safe because you control it does not survive contact with a client who pastes something unexpected into a field two years later. Escaping at the point of output is cheap, mechanical and does not require you to reason about where the data came from. Use nonces and capability checks for anything that writes. The broader case is in website security that protects your SEO.
Build accessibility in rather than bolting it on
A skip link, one H1 per page, headings that descend in order, visible focus states, real button elements for buttons, labels tied to inputs, and alt text that a human wrote. All of these are easier to put in while you are building the templates than to retrofit across a finished site, and most of them help search engines read the page as well. Accessibility and SEO covers the overlap.
What is a waste of effort
Building a theme options panel. Clients rarely use it, it becomes a support surface, and the settings end up configured once and forgotten. Hard-code the decisions and change them in code when they change.
Supporting every WordPress feature. If the site has no comments, no sidebars and no author archives, do not build them. Unused templates are places for bugs to hide.
Starting from a bloated starter theme. You inherit someone else’s abstractions and spend your first week removing them. Starting from a blank folder is faster than it sounds once you have done it twice.
Do not forget the editing experience
A theme that looks right on the front end and renders as unstyled chaos in the editor is a theme the client will quietly stop using properly.
Load a small editor stylesheet so the writing surface reflects your actual typography, measure and spacing. It takes an hour and it changes how people feel about the site they are maintaining.
Register your colour palette and type scale so the controls offered to editors are your values rather than the defaults. Remove the options you do not want used. An editor presented with a full colour picker will eventually use it.
Build a handful of patterns for the layouts the site actually needs, a call to action block, a two-column feature, a quote treatment, and the content team will use those instead of inventing arrangements that do not fit.
Decide deliberately what a non-developer may change. Everything you leave open is something you may have to repair.
Version control and deployment
The theme is code and deserves to be treated like code, which a surprising number of custom themes are not.
Keep it in a repository with the uploads directory and any generated files excluded. Never edit theme files through the WordPress admin editor. If you do, the next deployment overwrites the change and nobody will remember it was made there.
If you have a build step for CSS or JavaScript, commit the sources and decide explicitly whether the built assets are committed or produced on deploy. Both work. Doing it inconsistently does not.
Have a staging environment that matches production in PHP version and plugin set, because most theme failures come from a plugin interaction rather than from the theme alone.
Bump the theme version on every release and tie your asset cache busting to the file modification time, so a deploy invalidates caches without anyone remembering a manual step.
Write a short readme in the theme itself covering what is where, which files own which concern, and any deliberate oddity. The full-width template that prints no title is exactly the kind of thing that belongs in it. The person reading that file in two years may well be you, with no memory of why.
A sensible build order
- Create the folder structure and get a blank theme activating cleanly.
- Write theme setup and asset loading in
inc/, withfunctions.phpas the loader. - Build
header.phpandfooter.phpwith correct document structure and your fonts self-hosted. - Build one template properly, usually
single.php, including its template parts. - Extend to the rest of the hierarchy, reusing those parts.
- Add page templates last, and document any that omit the title.
- Move anything that creates content or data into a companion plugin before launch.
If you are deciding between this and a block theme, that comparison deserves its own treatment and it is coming next. If you are deciding between WordPress and a JavaScript front end entirely, headless WordPress with Next.js sets out the trade.
We teach this structure, hands on, as part of the SEO and WordPress training in Pune. If you would rather have it built for you, get in touch.