Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 72 additions & 23 deletions bin/dq-install
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ if (!file_exists($autoload)) {
}
require_once $autoload;

// The injection logic is a plain class in this package — required directly so
// it resolves regardless of the consumer autoloader's state.
// The injection logic lives in plain classes in this package — required directly
// so they resolve regardless of the consumer autoloader's state.
require_once $packageRoot . '/src/Config/RecipeOptions.php';
require_once $packageRoot . '/src/Config/RecipeBlocks.php';

use DrupalQuick\Config\RecipeOptions;
use DrupalQuick\Config\RecipeBlocks;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Process\Process;
Expand Down Expand Up @@ -213,27 +215,31 @@ foreach ($toRequire as $package) {

echo "\n✅ Recipe packages installed.\n";

// --------------------------------------------------------------- Options
// Each managed recipe may declare user-tunable options as native recipe inputs
// (recipe.yml `input:`). Now that the packages are unpacked we can read them.
// --------------------------------------------------------------- Options + Block catalog
// Two write-back operations on config.dq.yml; we do them in a single read/write
// pass to avoid multiple file round-trips.
//
// default → write them, commented, into the matching recipe entry
// in config.dq.yml. The user uncomments to enable; every
// input has a default, so an untouched file still works.
// --exclude-options → print them to the terminal instead, touching no file.
// Options: each managed recipe may declare user-tunable inputs (recipe.yml
// `input:`). Default: write them as a commented block under the recipe entry.
// --exclude-options: print to terminal only, don't touch the file.
//
// Collect [config key => input definitions] for registry recipes with inputs.
// Block catalog: each registry recipe may advertise placeable blocks in its
// composer.json extra.dq.recipe.blocks. Always written as a commented
// `# ── Available recipe blocks` section so the user knows what keys to use
// in homepage: > blocks: (and future placement targets).

// Collect options from unpacked recipe.yml files.
$recipeOptions = [];
foreach ($recipes as $recipe) {
if (is_array($recipe) && isset($recipe['name'])) {
$key = $recipe['name'];
} elseif (is_string($recipe)) {
$key = $recipe;
} else {
continue; // inline package spec — no stable key to locate in the file
continue;
}
if (!isset($registry[$key]['package'])) {
continue; // core/contrib path, or a key with no registry entry
continue;
}
$pkg = $registry[$key]['package'];
$short = ($pos = strpos($pkg, '/')) !== false ? substr($pkg, $pos + 1) : $pkg;
Expand All @@ -251,6 +257,28 @@ foreach ($recipes as $recipe) {
}
}

// Collect blocks from the registry for all installed registry recipes.
$recipeBlocks = [];
foreach ($recipes as $recipe) {
if (is_array($recipe) && isset($recipe['name'])) {
$key = $recipe['name'];
} elseif (is_string($recipe)) {
$key = $recipe;
} else {
continue;
}
if (!isset($registry[$key]['blocks'])) {
continue;
}
foreach ($registry[$key]['blocks'] as $blockKey => $blockMeta) {
$recipeBlocks["{$key}/{$blockKey}"] = [
'label' => $blockMeta['label'] ?? $blockKey,
'plugin' => $blockMeta['plugin'] ?? '',
];
}
}

// Print options to terminal when --exclude-options is set.
if (!empty($recipeOptions) && $excludeOptions) {
echo "\nℹ️ Optional recipe settings — set under `options:` on the recipe's entry\n";
echo " in config.dq.yml (defaults apply when unset):\n";
Expand All @@ -263,20 +291,41 @@ if (!empty($recipeOptions) && $excludeOptions) {
echo " - {$name}: {$description}{$defaultText}\n";
}
}
} elseif (!empty($recipeOptions)) {
$lines = explode("\n", file_get_contents($configFile));
$injected = [];
foreach ($recipeOptions as $key => $inputs) {
$before = $lines;
$lines = RecipeOptions::injectCommented($lines, $key, RecipeOptions::activeLines($inputs));
if ($lines !== $before) {
$injected[] = $key;
}

// Single read → inject options → inject block catalog → single write.
$needsOptionWrite = !empty($recipeOptions) && !$excludeOptions;
$needsBlockWrite = !empty($recipeBlocks);

if ($needsOptionWrite || $needsBlockWrite) {
$lines = explode("\n", file_get_contents($configFile));
$before = $lines;

$injectedOptions = [];
if ($needsOptionWrite) {
foreach ($recipeOptions as $key => $inputs) {
$prev = $lines;
$lines = RecipeOptions::injectCommented($lines, $key, RecipeOptions::activeLines($inputs));
if ($lines !== $prev) {
$injectedOptions[] = $key;
}
}
}
if ($injected) {

if ($needsBlockWrite) {
$lines = RecipeBlocks::injectCatalog($lines, $recipeBlocks);
}

if ($lines !== $before) {
file_put_contents($configFile, implode("\n", $lines));
echo "\nℹ️ Wrote commented recipe options into config.dq.yml for: " . implode(', ', $injected) . ".\n";
echo " Uncomment (remove the leading `# `) under each recipe to override its defaults.\n";
if ($injectedOptions) {
echo "\nℹ️ Wrote commented recipe options into config.dq.yml for: " . implode(', ', $injectedOptions) . ".\n";
echo " Uncomment (remove the leading `# `) under each recipe to override its defaults.\n";
}
if ($needsBlockWrite) {
echo "\nℹ️ Wrote recipe block catalog into config.dq.yml.\n";
echo " Uncomment the homepage: section (remove `# `) to compose the front page.\n";
}
}
}

Expand Down
90 changes: 90 additions & 0 deletions docs/quickthe-me.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Dogfooding: quickthe.me

The project site (quickthe.me) is built **with** Quick: scaffolded by the real
`dq-init → dq-install → dq:scaffold` flow, then hand-edited — exactly the
workflow we tell users to follow. The site lives in its own repo
(`quickthe-me`, `~/dev/quickthe-me`): a `drupal-quick/site`-shaped project
whose first commit is the untouched scaffold output, so every hand edit after
it is a reviewable diff. Sections: marketing front page, `/docs/*` (Basic
pages + a docs menu block in the sidebar region), `/generator` (the
tools/generator app inside the site shell), `/presets` (a `showcase` content
type + view; each card carries a complete config.dq.yml and real screenshots
of a site scaffolded from it).

This file records what the dogfooding surfaced and where it should flow back
into the packages.

## Fixed during the exercise

- **`hidden md:block` never becomes visible** (dq-starterkit): Drupal core's
`system.module.css` also defines `.hidden` and loads *after* the inlined
theme CSS, so the sidebar shell's site title was `display:none` at every
width. Fixed with `max-md:hidden`. Rule of thumb for starterkit and recipe
templates: never use Tailwind's bare `hidden` on elements a breakpoint
variant is meant to reveal.
- **recipe-project was not self-contained on Drupal 11.4**: core's Standard
recipe stopped shipping content types there, so nothing provided the node
`body` field storage and applying `project` without `blog` failed. The
recipe now ships core's `field.storage.node.body.yml` verbatim (a validated
no-op where it already exists) — the same class of fix recipe-blog got with
its `article_content_type` dependency.

## Should flow into the scaffold

- **Theme-enable default blocks** (the already-deferred cleanup, now clearly
needed): when `dq:scaffold` enables the generated theme, Drupal copies the
previous default theme's block placements into it — a broken-logo branding
block plus admin/tools menu blocks land in the header on every scaffolded
site. The scaffold should delete those three placements and put the main
menu block into the shell's `primary_menu` region (label hidden). Both
quickthe.me's `scripts/site-setup.php` and the screenshot factory's seeder
had to do this by hand.
- **Document the 11.4 standard-recipe change**: configs that need Basic page
must list `core/recipes/page_content_type` explicitly. Worth a comment in
`templates/config.dq.yml` and possibly a registry-level hint.

## Recipe opportunities (not built, recommended)

quickthe.me was hand-composed; each of these is a candidate to become a Quick
recipe so a future config could reach the same starting point:

- **docs**: a documentation section — dedicated `docs` menu, menu block in the
sidebar region with `/docs*` visibility, a docs page-shell variant (sticky
left nav + reading column), rich-text typography for code-heavy bodies, and
the code-block copy behavior.
- **showcase**: the gallery — content type (config text + multi-image), teaser
grid view (page + embed block), carousel/copy teaser templates. Generalizes
to any "cards with images and a copyable payload" gallery.
- **marketing-front**: hero (heading, tagline, copyable command, CTA pair),
alternating feature bands, and a teaser band — as advertised homepage blocks.

Before building a docs recipe, evaluate contrib: `search_api` (+ database
backend) for docs search, `toc_api`/`toc_js` for per-page tables of contents,
`linkchecker` for rot. Navigation needs no contrib — a core menu block with
depth covers it.

## Other observations

- **Sample content should ship with recipes** (existing catalog.js @todo,
reinforced): gallery screenshots required hand-seeding articles/projects
(`dq-shot`'s seed script mirrors the generator's dummy data). If recipes
carried a sample-content manifest, both the generator preview and a
`--sample-content` scaffold flag could draw from it, and screenshot
pipelines would be trivial.
- **Alpine.js integration**: the site vendors Alpine through the theme's Vite
bundle (npm dep, registered components, `Alpine.start()` before Drupal
behaviors decorate content). Worked cleanly; if more sites want
interactivity, an optional starterkit integration (or a `dq-alpine`
submodule pattern) could package this.
- **`Node::create(['path' => …])` does not reliably persist aliases** in
site scripts — create `PathAlias` entities explicitly.
- **Twig sandbox**: `FieldItemList::count()` is not an allowed method in
templates; use `|length`.

## Sync contract reminder

The generator gained the `quick` preset in its static catalog — mirrored in
`tools/generator/catalog.js` (source of record) and the site's bundled copy
(`quickthe-me` theme `src/generator/catalog.js`). The real fix remains the
registry emitting a shared JSON contract both consume (see
tools/generator/README.md).
29 changes: 29 additions & 0 deletions docs/static-deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,35 @@ 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.

### Previewing the export under DDEV (`--ddev-preview`)

```bash
ddev drush dq:static --ddev-preview
ddev restart # once, to activate the new hostname
# then browse https://static.<project>.ddev.site
```

The flag provisions a second vhost in the same DDEV project that serves the
export directory as plain files (no PHP handler), beside the live site — so
the export can be checked exactly as a host would serve it, and re-exports
are just a refresh away. It writes two files:

- `.ddev/nginx_full/static.conf` — a static-only nginx server block rooted
at the export directory.
- `.ddev/config.static.yaml` — a DDEV config override registering
`static.<project>` in `additional_hostnames`, so the user's own
`config.yaml` is never edited.

Both carry a `#dq-generated` marker: they are rewritten on every
`--ddev-preview` run until the marker line is removed, at which point the
tool leaves them alone (the same ownership convention DDEV uses for its
generated files). The command runs inside the web container, so it cannot
restart DDEV itself — hence the one-time `ddev restart`.

Caveat: DDEV config overrides *replace* list values. If the project already
declares `additional_hostnames` in `config.yaml`, fold the static hostname
into that list and delete `config.static.yaml`.

### Deploy credentials

`dq:deploy` runs inside the DDEV web container, so the credentials must be present
Expand Down
5 changes: 4 additions & 1 deletion docs/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ ddev composer exec dq-install

Reads `config.dq.yml`, registers any recipe VCS repos in `composer.json`, and `composer require`s each recipe package. `core-recipe-unpack` unpacks them into `recipes/`.

Afterwards it writes each recipe's available options — read from the now-unpacked `recipe.yml` — back into `config.dq.yml` as a **commented block under that recipe's entry**, so you can uncomment (remove the leading `# `) and edit to override a default. Nothing is enabled until you do; defaults apply otherwise. Pass `dq-install --exclude-options` to skip the rewrite and just list the options in the terminal.
Afterwards it rewrites `config.dq.yml` in a single pass:

- **Recipe options** — each recipe's user-tunable inputs (from its `recipe.yml input:` block) are written as a commented block directly under that recipe's entry. Uncomment (remove the leading `# `) and edit to override a default; every input has a default so an untouched file still works. Pass `--exclude-options` to print them in the terminal instead and leave the file untouched.
- **Block catalog** — a `# ── Available recipe blocks` section is injected (or updated) listing every block your installed recipes advertise, keyed as `<recipe>/<block>`. The section includes the `homepage:` snippet to uncomment and edit. It also serves as a reference for future placement targets (sidebars, banners, etc.); for now only `homepage:` is acted on by `dq:scaffold`.

> **Local dev note:** if recipe/theme packages are on your local machine rather than published to a Git remote, add them as path repos in `composer.json` before this step. VCS repos (GitHub) require DDEV to have GitHub auth — run `ddev auth ssh` first.

Expand Down
Loading
Loading