Demo Project
studiometa/foehn-demo is every feature Føhn ships, in one working WordPress theme.
It exists to be read and to be run. It is also what the framework's end-to-end tests run against, so every attribute in it is exercised against a real WordPress rather than against function stubs — which means the examples here are known to work, not merely known to compile.
Starting a project? Use the starter instead. It is the same theme with the demonstrations taken out.
Running it
composer create-project studiometa/foehn-demo foehn-demo
cd foehn-demo
ddev start
ddev launchAdmin at /wp/wp-admin, with admin / admin.
What is demonstrated where
| Attribute | File |
|---|---|
#[AsPostType] | app/Models/Product.php, app/Models/Testimonial.php |
#[AsPostMeta] | app/Models/Product.php — price, sale_price |
#[AsTaxonomy] | app/Taxonomies/ |
#[AsBlock] | app/Blocks/ — a container, a sidebar-driven one, a DTO one |
#[AsBlockBinding] | app/Bindings/ReadingTime.php |
#[AsSettingsPage] | app/Settings/ThemeSettings.php, with a Twig form |
#[AsRewriteRule] | app/Routes/HealthCheckRoute.php — GET /_health |
#[AsTemplateController] | app/Controllers/ |
#[AsContextProvider] | app/ContextProviders/ |
#[AsImageSize] | app/ImageSizes/ |
#[AsMenu] | app/Menus/ |
#[AsAction] / #[AsFilter] | app/Hooks/ThemeHooks.php |
| Arrayable DTOs | app/Data/HeroContext.php |
The theme's namespace is Demo\ rather than App\, which is the only thing in it you would not copy: App\ is what the starter uses and what a project of your own should.
What is not here
ACF. It needs ACF Pro, a paid plugin CI cannot install, so an ACF block here would be a path nothing ever runs — which is exactly what it was before the integration moved out. studiometa/foehn-acf carries its own examples.
The tests are the point
composer test:demo # the PHP suite
npm run -w @studiometa/foehn-demo test:run # browser tests
./tests/smoke/run.sh # against a started ddevtests/smoke/ is what makes this package worth having. The PHP suites run against WordPress function stubs, so a discovery that registers nothing at all still passes them — on 2026-08-19 that was 1409 passing tests and a fatal error on every front-end page of the site.
The smoke test drives real requests against a real WordPress and asserts inside it: that the post types exist, that the meta is registered against the right subtype, that a bound block renders its computed value, that GET /_health reaches its handler, that the settings page appears under Appearance. CI runs it twice per change, on a cold discovery cache and again on a warm one, because restoring items and scanning them are different code paths.
Every assertion in it was checked to fail when its feature is removed. An assertion that cannot fail is worse than none.