Open-source project
elementary/website avatar
elementary/website

elementary.io website: PHP templates at the root, Gulp and Webpack for assets

The elementary.io website

1,268 stars717 forksPHPMIT

At a glance

What is it?
The elementary.io website is a small PHP site with a hand-rolled templating convention, a Gulp and Webpack asset pipeline, Composer for the backend and a Node build wrapper. It is a good model for a marketing site of modest size, and a poor base for anything that needs a framework.
Who is it for?
Adopt this repository if you want to see a small PHP site with no framework, pages in the root, a two-step build and a translation workflow, and you are willing to own the conventions by hand. Do not adopt it as a starting point for a product application, because the whole design is a website for one organization with hand-written PHP, a bespoke template array and a small set of conventions that a new maintainer has to read before touching anything.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Pages are root-level PHP files that include a header and a footer

The templating story is the first thing to understand, and it is deliberately small. The project documentation describes a PHP-based templating system where pages go in the root as.php files. A page requires the preload file, sets any variables it needs, includes the header template at the top of the page and the footer template at the bottom. The documentation points at code-of-conduct.php as the example. The page variables it names are $page['title'], $page['description'] and $page['author']. There is no controller, no view layer abstraction, and no template engine, and the root directory of the repository reflects that: index.php, about pages such as team.php, press.php, support.php, privacy.php, oem.php, store.php, brand.php, open-source.php, developer.php, get-involved.php and thank-you.php all sit next to each other, along with 403.php, 404.php and 410.php for status responses. The templates themselves live in _templates/, the styles in _styles/, the scripts in _scripts/, the images in _images/, the translations in _lang/, the backend in _backend/, and the tests in _tests/. If you are used to a framework, the whole mental model is one flat directory of pages and a convention you follow by hand.

The stated philosophies: no frameworks, mobile-first, graceful degradation

The documentation lists its architecture and philosophies in five lines, and they read as constraints rather than aspirations. HTML, CSS and JS. A super simple PHP-based templating system. No or minimal frameworks, with the note to keep it simple. Graceful degradation, with the instruction not to be held back by crappy browsers. Mobile-first. These are the decisions that explain the rest of the repository, and they also explain its limits. A no-framework rule is a defensible way to keep a site buildable by whoever is on call, and it is also a decision that pushes complexity into conventions, because anything you would normally get from a framework, routing, templating, asset pipelines, you have to hold onto yourself. The mobile-first and graceful degradation rules explain the presence of .browserslistrc and of a stylelint configuration in the repository root. What is not stated is which browsers are actually supported today, and since the philosophy is explicitly about not being held back by bad browsers, the answer changes over time and lives in the browserslist file rather than in prose.

The build is Gulp for styles and images, Webpack for scripts

Two asset pipelines run in parallel, which is the most distinctive thing about the toolchain. The dependency list in package.json includes gulp, gulp-postcss, gulp-rename, gulp-rev, gulp-imagemin, gulp-cached, gulp-changed, cssnano, postcss-preset-env, glob and scriptjs for the styles and images side, and webpack, webpack-manifest-plugin, eslint, stylelint and their configs for the script side. The npm scripts confirm the split: a build entry that runs gulp, a separate images:rebuild target, and a watch target that also runs gulp watch. The whole set of npm scripts is below, copied from the scripts block of the package manifest.

json
  "scripts": {
    "install:submodule": "git submodule init && git submodule update --force --rebase",
    "install:composer": "cd _backend && composer install",
    "postinstall": "npm run install:composer",
    "lint": "sh _tests/css.sh && sh _tests/javascript.sh && sh _tests/php.sh",
    "build": "gulp",
    "build:images": "gulp images:rebuild",
    "start": "php -S 0.0.0.0:8000 router.php",
    "watch": "gulp watch"
  },

Read the lint entry closely: it is three shell scripts run in sequence, which is the entire check suite the project defines. The naming mvp for the package, and the version 0.4.0, tell you how the team sees the maturity of the whole thing. A gulp-rev step produces hashed asset filenames for cache busting, and a Webpack manifest plugin produces the mapping between those names and the files, which is how the PHP pages can reference versioned assets without hardcoding hashes. Two pipelines do mean two configuration surfaces, one in gulpfile.mjs and one in webpack.config.js, and any contributor touching assets has to know which tool owns which asset type.

The npm start script serves the site with the PHP router

There is a development server and it is worth knowing exactly what it runs, because the port and the entry point are both visible in the scripts section of package.json. The start script is a single line, and it runs the built-in PHP server bound to all interfaces on port 8000 with router.php as the router. That is the whole local stack for the backend, and it means the site can be run without nginx, which is consistent with the repository keeping an nginx.conf file for production. The router script is what maps incoming requests onto the root-level page files, and the presence of 403.php, 404.php and 410.php in the root is what the router dispatches to. If you want to see the site locally, the practical order is to install the Node dependencies, let the postinstall step fetch the Composer packages for the backend, run the build so the styles and scripts exist, and then start the PHP server on port 8000. The build step is separate and is what regenerates the styles and scripts, so a first run that shows unstyled markup usually means the assets were never built.

npm install pulls in Composer and a git submodule as a side effect

The postinstall hook is the detail that most surprises new contributors. The scripts section defines an install:submodule step that runs git submodule init and then git submodule update with force and rebase, an install:composer step that changes into _backend and runs composer install, and a postinstall that calls the Composer step. That means npm install is not a Node-only operation. It requires git, it mutates the working tree by initializing submodules, it needs PHP and Composer on the machine, and it reaches the network a second time for the backend dependencies. The repository root does contain a .gitmodules file, so the submodule is a real thing here, and _backend is a separate dependency tree. For CI, this coupling matters: any build definition for this site has to provide Node, PHP, Composer and a git client, and the environment needs to tolerate a git command that rewrites submodule checkouts. The documented requirement for Node is given in the engines field as 18.17.1 or newer, and the package is declared with type module, so the config files are ES modules rather than CommonJS.

Where this architecture stops working

There are clear boundaries, and they are a function of the design rather than accidents. The no-framework rule means there is no shared component model, so a change to a header or a footer is a change to the shared template in _templates/, and any consistent behaviour across pages has to be enforced by convention and review. The flat page model means there is no route table, so adding a URL with a path segment rather than a file is not a matter of creating a file. Localization is handled by the _lang directory and a separate TRANSLATE.md, and the project carries a translation status badge, so a change to shared markup has a translation tail that a small team can underestimate. The tests directory holds shell scripts rather than a test framework, and the lint command runs a CSS script, a JavaScript script and a PHP script in sequence, which means the checks are linting, not behavioural testing. The README also states a hard rule for contributors that there is no dummy content in master, ever, and a small diff is best diff, which is a reasonable review culture but also means larger changes get pushed back. And the README describes the project itself as a focused, minimum viable product for a website that accompanied the release of elementary OS Freya, which is a fair description of its ambition.

How this differs from a framework-based site and from a static generator

The two obvious alternatives approach the same problem from opposite ends, and it helps to see why the project sits between them. A static site generator would give you content files, a build to HTML and no runtime, which is simpler to host and faster to serve, but it moves the work into the content layer and gives you no server-side pages when you need them, for example a page that reflects a session or a form that posts back. A PHP framework would give you routing, an ORM, a template engine, a form layer and an admin surface out of the box, at the cost of a dependency tree and a conventions layer that the project has explicitly chosen not to have. The elementary site does neither and instead keeps server-side PHP pages with a small convention, plus a JavaScript front end that is compiled by Webpack. That choice pays off when pages are few, mostly static, and edited by a small team who want to read one file per page. It stops paying off when the site grows state, when a designer needs reusable components, or when non-developers need to change content without touching PHP.

Licence, releases and the age of the release tags

The repository is under the MIT license, with LICENSE.md at the root, and the package manifest repeats the MIT identifier and lists elementary as the author. MIT is permissive and covers the code, the styles and the templates, but the branding assets in _images/ and the fonts directory are a separate question the licence file does not answer for you, and the site is a representation of the organization rather than a generic template, so the value of the repository for most readers is the pattern rather than the pixels. On maintenance, the important detail is the difference between the two signals you get. The last push to the repository was on 2026-10-15, so the site code is being worked on. The release tags tell a different story: 5.0 dated 2018-10-19, 0.4.1-20180130 dated 2018-01-30, and 0.4.1 dated 2017-05-17, and the tag names are elementary OS releases, not website releases. So the tags are the OS release train passing through this repository, and the manifest version of 0.4.0 is the website's own number, which has barely moved. There is also a SECURITY.md with a disclosure policy and contact, and a translation workflow, both of which signal an ongoing project rather than a snapshot, but neither of them is a changelog for the site itself.

Editorial conclusion

Adopt this repository if you want to see a small PHP site with no framework, pages in the root, a two-step build and a translation workflow, and you are willing to own the conventions by hand. Do not adopt it as a starting point for a product application, because the whole design is a website for one organization with hand-written PHP, a bespoke template array and a small set of conventions that a new maintainer has to read before touching anything. Before you build on it, check that the Node and PHP versions you run match the engines field, that the submodule and Composer steps still work, and that the page variables and template paths still match the pages you intend to copy.

Frequently asked questions

How do I run the elementary.io website locally?

The package.json scripts define a start entry that runs php -S 0.0.0.0:8000 router.php, so the built-in PHP server serves the site through router.php on port 8000. The build entry runs gulp, and the watch entry runs gulp watch.

What does npm install do on this repository?

It is not a Node-only step. The postinstall script runs composer install inside _backend, and there is an install:submodule script that runs git submodule init followed by git submodule update with force and rebase, so git, PHP and Composer must all be available.

How are pages organized in the elementary.io website?

Pages go in the repository root as .php files. A page requires _backend/preload.php, sets variables, then includes the header template at the top and the footer template at the bottom, with page variables such as $page['title'], $page['description'] and $page['author'].

What license does the elementary.io website use?

The repository is under the MIT license, with LICENSE.md in the root, and the package manifest lists the MIT license identifier as well.

Official sources

  1. elementary/website on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/elementary-website.svg)](https://hysenlabs.com/projects/elementary-website)

Was this useful?