drupalquick's goal is quick in two senses: fast provisioning and a fast end result. Part of that is the ability to ship the finished site as a static, HTML-only build — provision Drupal locally, scaffold a theme and content, remove every trace of Quick, then deploy a performant static site.
This is done with Tome (the tome_static submodule) driven
by the drush dq:static command. No front-end framework is involved — the
output is plain HTML/CSS/JS rendered by Drupal itself.
- Drush-driven (
drush tome:static) — the same execution model the rest of drupalquick uses. - Plain HTML output. Tome renders every anonymous-accessible route and
entity canonical path through Drupal's own HTTP kernel, then collects the
referenced stylesheets, scripts, images (incl.
srcset), favicons, and pager links. The theme's builtdist/main.css/dist/main.jsare captured as-is. - Reinforces "no footprint of Quick." The deployed artifact is just static
files — no Drupal, no PHP, no drupalquick. Drupal + Tome remain locally as the
authoring/build environment;
dq:cleanupstill removes Quick itself. - Deploy targets built in — Tome documents GitHub Pages, Netlify, Render, and more.
- Drupal 11 support — Tome
8.x-1.14(Feb 2026) requires^10 || ^11and is covered by Drupal's security advisory policy. Note it is minimally maintained / feature-complete, so treat it as stable-but-static, not actively evolving.
Static export spans three concerns, each with a natural home:
| Concern | Home | Why |
|---|---|---|
| Project settings (deploy target, base URL) | config.dq.yml static: block |
The single file the user already edits |
| The operation (install Tome, build check, export, emit deploy config) | drush dq:static command |
Recurring/imperative; recipes cannot run an export |
| Capability install (Tome module + its config) | a recipe (future) | Declarative/composable — but premature for one contrib module |
So the current design is config + command, not a recipe. A recipe can't
perform the export, so the command is required regardless, and a recipe that only
does install: [tome_static] is too thin to justify today. Once the recipe
ecosystem is externalized and self-describing (see
extensibility.md), "static export capability" becomes a
natural candidate to extract into a recipe-static.
dq:cleanup deletes config.dq.yml, but static export is recurring — you
re-export whenever content changes, often long after Quick is gone. So settings
cannot live only in config.dq.yml. dq:static therefore persists the
resolved settings into Drupal config (drupalquick.static) on first run.
Resolution order:
drupalquick.staticconfig (survives cleanup) — wins if present.- Otherwise the
static:block inconfig.dq.yml— seeds the first run.
This is also why a recipe is the eventual right home: it would write that persisted config declaratively.
# config.dq.yml
static:
target: "netlify" # netlify | github | none
uri: "https://example.com" # base URL for absolute links (optional)# after the site is scaffolded and the theme is built
ddev drush dq:static
# or override the base URL ad hoc:
ddev drush dq:static --base-url=https://example.com
# then publish the export to the configured target:
ddev drush dq:deploydq:deploy is a separate command (so generation and publishing each carry their
own flags). It reads the target from the persisted drupalquick.static config
(seeded by dq:static), writes the target's deploy config, and pushes. Only
Netlify is automated for now: netlify deploy --prod --dir=html, using a
globally installed netlify CLI if present, otherwise npx netlify-cli. GitHub
Pages deploys via its own workflow (git push), so for that target dq:deploy
just writes .github/workflows/deploy-pages.yml. It is loosely coupled to the
build: if html/ is missing it tells you to run dq:static first rather than
regenerating implicitly.
dq:deploy runs inside the DDEV web container, so the credentials must be present
there — and they are secrets, so they must stay out of version control. The
mechanism:
-
dq-init --ddevdelivers.ddev/.env.web.example(keys, no values) and adds.ddev/.env.webto.gitignore. -
Copy it and fill in your token:
cp .ddev/.env.web.example .ddev/.env.web # edit .ddev/.env.web → NETLIFY_AUTH_TOKEN=... (NETLIFY_SITE_ID optional) ddev restart
DDEV loads .ddev/.env.web into the web container, where netlify deploy picks
the token up. Never put the token in config.local.yaml (its
web_environment is committed) or anywhere tracked.
Don't want secrets in the container at all? Skip dq:deploy and deploy the export
from the host, where netlify login already stored credentials:
netlify deploy --prod --dir=htmldq:static will:
- Resolve settings (persisted config, else
config.dq.yml). composer require drupal/tomeand enabletome_staticif not already present.- Persist
target/uritodrupalquick.static(so it survivesdq:cleanup). - Preflight the active theme — abort if the Vite dev marker is present, warn if
dist/main.cssis missing. - Run
drush tome:static(with--uriif configured).
dq:deploy then:
- Confirms
html/exists (else tells you to rundq:static). - Writes the deploy config for the target (
netlify.tomlor.github/workflows/deploy-pages.yml). - Pushes the export to the target (Netlify automated; GitHub via its workflow).
Output lands in html/ (Tome's default; override with
$settings['tome_static_directory'] in settings.php).
- Dynamic features don't survive static. Forms, search, comments, and anything authenticated are gone. The blog/content-display starter is fine; search would need a static approach (e.g. Pagefind/Lunr) and forms a third-party (Netlify Forms / a function).
- Vite dev mode must be off. If the
.vite-devmarker exists, the theme injectslocalhost:5173dev-server tags, which the export would capture.dq:staticaborts if it detects the marker. Runnpm run buildfirst. - Build the theme first. Without
dist/main.*, the export is unstyled. - Base path / rewrites vary by host. Subdir vs root and clean-URL rewrites differ per platform; the emitted deploy config is a starting point.
- Tome maintenance. Stable on D11 but feature-complete/fixes-only.
- Static Suite — actively maintained but built around exporting JSON for an external SSG (Astro/Gatsby/Eleventy), which reintroduces a JS build chain — contrary to the "no framework / keep it light" goal.
- Static Generator / Static / Static Node Generator — partial or on-demand exporters with smaller communities and weaker D11 maintenance.
- Crawl/mirror tools (wget/httrack) — crude and fragile with Drupal asset URLs.
Tome remains the best fit for full-site, framework-free HTML output.
Today deployStatic() handles one automated target (Netlify). Don't
pre-abstract for one target — but it would balloon if more were added as
inline branches, so here is the intended progression:
-
At target #2: turn
deployStatic()into a thin dispatcher and give each target its own small method, so each one's quirks (auth checks, flags) stay isolated:private function deployStatic(string $target): int { return match ($target) { 'netlify' => $this->deployNetlify(), 'vercel' => $this->deployVercel(), 'github' => 0, // deploys via git push; no-op here default => $this->warnUnsupported($target), }; }
deployStatic()stays ~6 lines regardless of target count. -
If targets trend homogeneous (most static hosts are "run a CLI against the output dir":
netlify,vercel,surge,wrangler pages,aws s3 sync,firebase), promote to a data-drivendeploy-targetsmap — the same pattern asrecipe-registry.json— so a new target is data, not code. Keep a method escape hatch for non-CLI targets: GitHub Pages deploys via git push, not a one-line command. -
Only for external contributions: a
StaticDeployerInterfacewith one class per target and discovery. This is the textbook answer but overkill now, and it fights the runtime context —dq:staticruns in a Drush command with an unreliable container, so heavy DI/plugin discovery is awkward. Reserve it for when packages must contribute their own deployers (see extensibility.md).
- Now:
config.dq.ymlstatic:block +dq:staticcommand (this prototype) — installs Tome idempotently, persists settings, exports, emits a deploy template. - Next: richer deploy targets and a
--buildflag that runs the theme build before exporting. - Later: extract Tome install + config into a
recipe-staticonce recipes are externalized, and consider a static search integration (Pagefind) for the lost dynamic search.