flat-file · assembly order
Getting started.
Seven steps to a static site: one dependency, one page, one layout, one build. The example is the one this page was verified against — PHP 8.5.11 and Composer 2.10.3 — not a sketch of it.
Check the two versions that matter
You need PHP and Composer, and nothing else. PHP runs at build time; the output is plain HTML, so the host that serves the site does not need PHP at all.
The versions used here: PHP 8.5.11 (Homebrew, CLI) and Composer 2.10.3. PHP 8.3 or later is a safe floor for what follows, because the pinned revision pulls Symfony Console 7 and CommonMark 2.
The one caveat that decides the install. The published tags
1.0.0–1.0.3 and the master branch — the
branch the source README's sample composer.json names as
dev-master — declare php ~7.1. On current PHP,
Composer refuses them outright:
- slogsdon/flat-file[1.0.0, ..., 1.0.3] require php ~7.1
-> your php version (8.5.11) does not satisfy that requirement.
The same applies to composer create-project
slogsdon/flat-file-starter: the starter requires ^1.0, so
it fails the same way. The working constraint is the
dev-code-highlight branch, pinned to a commit in step
2.
Write composer.json, then install
This is the README's sample file with one substitution: the dependency is the
branch that installs on current PHP, not dev-master. The
scripts and config sections are optional — they only
give the two commands shorter names, and process-timeout lets the
server script outlive Composer's 300-second default.
{
"name": "user/site",
"description": "",
"require": {
"slogsdon/flat-file": "dev-code-highlight"
},
"scripts": {
"build": "flat-file build",
"start": "flat-file serve"
},
"config": {
"process-timeout": 0
}
}
Then pull down dependencies:
composer install
That resolves 22 packages and installs slogsdon/flat-file
(dev-code-highlight 3ba5285) into vendor/. Commit the
generated composer.lock; without it a later install can resolve
a different commit of the same branch.
If you drop the scripts section, call the binary directly:
vendor/bin/flat-file build and vendor/bin/flat-file
serve.
Add pages
Create a pages directory next to composer.json. This
is the whole homepage:
<?php /* pages/index.php */ ?>
<h1>Hello World</h1>
A second page in Markdown needs no front matter and no wrapper:
<!-- pages/about.md -->
# About
flat-file reads this file and writes /about.
Routing is the file system. The filename, minus its extension, is the URL; a nested directory is a nested path. These are the paths this example produced:
| Source file | URL | Build output |
|---|---|---|
pages/index.php | / | dist/index.html |
pages/about.md | /about | dist/about/index.html |
pages/notes/hello.php | /notes/hello | dist/notes/hello/index.html |
pages/404.html.php | /404.html | dist/404.html |
public/assets/site.css | /assets/site.css | dist/assets/site.css |
The last two rows are the two exceptions to the
<directory>/index.html rule. A page whose filename already
carries an extension is written to that exact filename, which is how a
404.html.php page produces a root 404.html; and
public/ is copied through verbatim, keeping its own paths.
Print, don't return. The README says a PHP page may return its
content as a string (<?php return 'hello world';). In the
revision pinned here that page builds to a zero-byte file: PHP pages are
rendered through the template engine, which captures output and discards a
return value. Use markup or echo.
Wrap the pages in a layout
Without a layout every page has to carry its own
<html>, <head>, and navigation. Put the
shared shell in resources/partials/, which the generator registers
as a template folder named partials:
<?php /* resources/partials/layout.php */ ?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?= $this->e($title) ?></title>
<meta name="description" content="<?= $this->e($description) ?>">
</head>
<body>
<main><?= $this->section('content') ?></main>
</body>
</html>
A PHP page then opens by declaring the layout and the values it needs. Everything
after that call is captured as the content section the layout
prints:
<?php /* pages/index.php */ ?>
<?php $this->layout('partials::layout', [
'title' => 'Home',
'description' => 'A flat-file example.',
]); ?>
<h1>Hello World</h1>
<p>Rendered by flat-file through a shared layout.</p>
Markdown pages reach the same mechanism through front matter, with one
difference in the layout: a Markdown page is handed its converted body as a
$content variable, not as a section. So a Markdown layout prints
<?= $content ?>, and the page names it by filename:
<!-- pages/about.md -->
---
layout: partials::doc
title: About
description: About this example.
---
# About
Written in Markdown, wrapped by the same layout.
Two layouts are fine, and so is one layout that prints both
<?= $this->section('content') ?> and
<?= $content ?>. What costs something is mixing them up: a
Markdown page pointed at a layout that prints only the content
section builds successfully and emits an empty <main>. That
was verified here, and it is silent — nothing in the build output warns.
A layout may also name a partial for repeated fragments, e.g.
<?= $this->insert('partials::nav') ?>; that form was not
exercised here.
Build the static site
composer build
or, without the Composer script, vendor/bin/flat-file build. The
output is four lines, and the last one is the only success signal:
Discovering routes...
Generating files...
Copying public files...
Finished
The default destination is dist/ in the project root. Pass a
different directory as the first argument — composer build --
dist/site — which is the form this microsite's own build script uses.
Then upload that directory. It is the entire site.
Two things the generator does not do for you: it does not render a page that no
file matches, and it does not invent redirects. A URL with no source file is
simply absent from the artifact, so a static host's own 404 handling applies
unless you build a 404.html page as in step 3.
Look at it before you build
composer start runs PHP's built-in server on
http://localhost:3000, with public/ as the document
root — or your own public/index.php if you write one, and the
package's src/router.php otherwise. The README's custom-entrypoint
example is the file to copy if you need one:
<?php declare(strict_types=1);
$uri = $_SERVER['REQUEST_URI'];
if ($uri !== '/' && file_exists(getcwd() . '/public' . $uri)) {
return false;
}
require getcwd() . '/vendor/autoload.php';
new FlatFile\Application;
Not verified here: the server was not started while writing this
page. flat-file serve --dry-run prints the command it would run —
php -S localhost:3000 -t <project>/public
<project>/vendor/slogsdon/flat-file/src/router.php — and that is
all that was checked. The same tree, served by a web server pointed at
public/, is what the README calls the flat-file CMS mode.
Deploy the artifact, not the project
There is nothing project-specific to deploy. dist/ is HTML, CSS,
and whatever else you put in public/, so any static host or
object-storage origin will serve it; PHP is only needed in the build step.
That is also the shape used here: this microsite and the collection hub both
build with composer build and publish the output as static assets
under a path prefix.
The starter's README documents a Now (Vercel) Docker deployment. It is
historical: now.json, a composer:latest image, and
^1.0 dependencies that no longer install on current PHP. Read it
as an example of the file layout, not as a deployment recipe, and don't copy
it.
Not verified here: no deploy, preview, or Cloudflare command was run while writing this page.
where to go
after the build-
Project introduction
What flat-file is, the two repositories, and the status of the tagged releases.
-
Source on GitHub
The README this quick start is derived from, plus the parsers, commands, and router.
-
flat-file-starter
The example project. Historical deployment notes; not a current install path.