NAME
App::FuguWeb::Site - the whole build, from the description to the pages
SYNOPSIS
use App::FuguWeb::Site;
my $site = App::FuguWeb::Site->new(
config => $config,
out => 'web/build',
);
unless ($site->build) {
my $missing = $site->missing_tool;
die "$missing not found\n" if defined $missing;
die "the build failed\n";
}
$site->clean;
DESCRIPTION
One method renders the site. Each step reports its own failure and stops the build: a site that is half rendered must not look like a success.
The build reads nothing from the network, writes nothing outside the output directory, and gives the same bytes for the same checkout.
METHODS
new
App::FuguWeb::Site->new(
config => $config, # required
out => $dir, # required: the output directory
log => $logger, # default: Fugu::Log->default
render => $render, # default: one over the same description
)
config, staging
my $staging = $site->staging;
my $chinese = $site->staging($config->language('zh-Hans'));
The site description, and the mdoc staging directory, or the directory of one App::FuguWeb::Language inside it, under the path of its tree.
missing_tool
The renderer that failed the probe of the last "build", or undef. The build probes once and names the tool in the log; the caller maps this value to its own exit code, so an operator learns which package to install.
build
Render the whole site:
check that every renderer is installed, and name the first one that is not;
refuse an output directory that no build may own, or that holds a source;
hold each manual and each Markdown page of each language to UTF-8, per WEB-SITE-5, and name the source that is not;
lint every mdoc source of each language;
refuse a symlink on a path that the build writes, with each name folded per WEB-OUTPUT-17, and create the output directory, the tree of each language below its path, and the staging directory of each language;
copy the mdoc source of each language into the staging directory of that language, under its staged name. A source of a translation takes the staged name of its namesake;
copy the base stylesheet and every asset;
write the key directory, when the description names one;
render each
pageblock into each language tree, from the values of that language, then the pages of the top level: the top-level 404.html when the description holds that page, and the forward index.html. The top-level 404.html holds the body of the 404.html of each tree, as "not_found" in App::FuguWeb::Fragment fits it;render one page for each manual in each group, into each language tree, from the source of that language. "manual" in App::FuguWeb::Fragment removes the head and the foot of
mandoc, and opens the page with its title, per WEB-MANUALS-9;remove the staging directory, every file that the site dropped, and every directory that is then empty.
The last step is what makes a second build over the same directory right. A manual that was renamed leaves its old page behind, and a site that carried both would fail its own check. The build owns the output directory, so it owns the removal too.
The build reads each body fragment and each footer through "decode" in App::FuguWeb, and it writes each page through "encode" in App::FuguWeb, per WEB-SITE-5.
The default language comes first in the order of the languages, per D-05, so each page of the default tree renders before its namesakes. The build keeps the outline of each page of the default tree. Each page of another language must hold that outline, and it takes the id of each heading through "follow" in App::FuguWeb::Fragment, per WEB-OUTLINE-1 and WEB-OUTLINE-2. Each link of the page to one of its headings then names the new id, per WEB-OUTLINE-4. A link of the class Sx of mandoc(1) finds its heading by its text, and each other link by its old id. A page whose outline differs fails the build. The message names the page, the language and the position of the first heading that differs.
The build owns a plain file at the top level of the output, and a plain file directly in the tree of a declared language, per WEB-OUTPUT-1. The path of a tree is the tag of its language in lowercase. Below the root the build also owns a file that the site names, and one that takes a shape of the key directory. It keeps every other entry and reports it. An operator directory in the output therefore survives a build, empty or not. The tree of a language that the description dropped is no tree of the site, so the build keeps it and reports it, per WEB-OUTPUT-4.
A description with no keys block owns no shape of the key directory and no directory of the key tree. A site of another maker holds .well-known/security.txt too, and the clean must not delete that site.
The build removes an empty directory that it owns: one that the site holds a name below, one of the key tree, and the tree of a declared language. The output directory itself always stays. The clean reads the same three tests, so a build can never remove a directory that the clean refuses. rmdir refuses a directory that still holds a name, so the walk needs no second test.
The method returns true on success, and undef with a message in the log otherwise.
The output directory is created on purpose. The recipe that this replaced got it as a side effect of the staging mkdir, which would have broken the moment the staging step moved.
clean
Remove the output directory. The method returns true when the directory is gone, whether or not it was there to begin with.
clean removes a built site and refuses anything else. It deletes a tree without asking, so the one thing it must never do is delete a tree that no build made.
A plain file of the top level is one that a build writes, whatever the description names today. So is a plain file directly in the tree of a declared language: a renamed manual leaves its old page there. A file below the root must be one that the site names, or one of the shapes that the key directory takes. A directory must hold a name of the site, be the tree of a declared language, or be a directory of the key tree. A symlink is never a thing that a build wrote. The clean refuses every other entry, and it names it.
The clean and the prune read one predicate, so a build can never remove a file that the clean refuses. The predicate reads each name as it stands, and it folds no name, per WEB-OUTPUT-17. On a file system with case, EN is then no tree of the language en, so each one owns less.
The staging directory is a build detail, and the build writes one flat directory of sources into it for each language. A tree of another shape belongs to whoever made it.
With --out the description still names the key directory, so clean reads it when it can. A description that does not load names no page. The files of the top level and of the declared language trees are then the whole site that clean knows.
That fallback is weak, so clean asks for one more thing: the target must hold the stylesheet. Every build writes it, so a target without it is the output of no build. The target guard is the first line, and this is the second.
A description that does not load still names four things: the source directory, the output directory, the tag of each language block, and the name and org of its keys block. Fugu::Config keeps every setting and block that it read before the fault, so each guard answers for the real directories of that project. The source directory and the key directories are then the only sources that the target guard knows. A fault above the keys block hides it, and clean then refuses the key tree of its own output. Remove that tree by hand. A fault above a language block hides that tree the same way.
A description that drops its keys block strands the published tree. The site names nothing there any more, so the build keeps it, clean refuses it and the checks report it. Remove the directory, or name the block again.
THE OUTPUT DIRECTORY
Both build and clean refuse a target that no build may own. They refuse the root of the filesystem, the home directory, the project root, and any directory that holds the project. They refuse the source directory, and every directory of it. --out reaches both commands, and the out_dir setting it overrides is checked in the description, so the guard has to sit here as well. Each refusal folds each path, per WEB-OUTPUT-17. On a file system without case, the target WEB is the source directory web. The exception of the output directory of the description compares the paths as they stand, so the fold can only make the guard refuse more.
A description that names neither directory takes the two defaults.
The source directory needs the rule because it reads like a built site: plain files of the top level. The key directory is the worst case. Each key file sits at its top level, where the clean takes a plain file, so a clean of it would take the trust anchor of every release.
The output directory is the one exception below the source, and it needs to be: the default output directory is web/build, and it sits inside the default source directory web. The description names that path, so a build owns it. Nothing else below the source is a build's.
Both commands also refuse a target that holds a source, per WEB-OUTPUT-18. The prune removes each file of the top level that the site does not name, and clean removes the whole target, so either one would remove the source. The list comes from sources of App::FuguWeb::Config: each body, footer, Markdown page, manual and POD sidecar of each language, and the stylesheet that the description names. The source directory, each key directory and the dir of each group in each language are sources too. A target inside a key directory or inside the dir of a group is refused as well. The exception never covers these refusals. The message names the target and the first source that it meets, so a file comes before the directory that holds it.
The rule is deliberately not "inside the project". The tests and the CI both build into a temporary directory outside it.
A .. in the target is resolved before the comparison. File::Spec->canonpath leaves it alone by design, and a guard that compared the uncollapsed form would let <project>/.. through as a directory it had never seen.
THE STAGING DIRECTORY
mandoc decides between a local link and a link to the manual host by looking for a file named %N.%S in its working directory. Every mdoc source is therefore copied into one directory for each language under its "staged_name" in App::FuguWeb::Manual, and mandoc runs there.
The directory lives inside the output directory, under .man/<path>, where the path is that of the tree of the language, and the build removes it. A plain file directly in .man, or a directory there of another name, is somebody else's, and the build refuses it. Staging is a build detail: a published tree that carried it would serve the mdoc sources beside the pages made from them.
THE KEY DIRECTORY
A description with a keys block publishes the public keys of an organization. The build copies each key file and the manifest pair as they stand. It generates the KEYS file, the human page, the Web Key Directory tree and security.txt. App::FuguWeb::Keys holds the wiring, and the Fugu modules hold every generic part.
The site build cannot sign, so the manifest pair is a source file. A build that signed would prove that the builder holds the key, and never that the release does.
A description with no keys block writes no key directory, so every site that predates it keeps its output.
ASSETS
An asset is any file directly in the source directory that the build does not render: not a *.body.html fragment, not Markdown, and not a dot file. robots.txt and CNAME therefore need no entry in the description, and a CLAUDE.md beside them is not published.
Markdown in the source directory is either a page source, which a page block names and lowdown renders, or notes for the maintainers. Neither belongs in the output as it stands.
THE STYLESHEET
The base stylesheet ships with the tool, at share/fuguweb/style.css, and "share_path" in Fugu::File finds it: in a checkout through this module's location, or under the share tree of an installed App-FuguWeb distribution. The search never reads the working directory, so a share/fuguweb/style.css of the project does not replace the sheet. The stylesheet setting overrides the search.
A stylesheet that is not found fails the build and names the path. A site with no stylesheet must not look like a success.
SEE ALSO
App::FuguWeb, App::FuguWeb::Config, App::FuguWeb::Keys, App::FuguWeb::Render, App::FuguWeb::Fragment, App::FuguWeb::Page, App::FuguWeb::Index, Fugu::File
AUTHOR
Dick Olsson <hi@senzilla.io>