Add front page, nav rebuild, and CI workflow

- Home controller with hero and services sections
- Nav rebuilt on install: Home, Services, About, FAQ, Contact (CTA), Book An Appointment (CTA)
- Entrypoint uses IS_SETUP check so failed installs retry on restart
- Gitea Actions workflow builds and pushes multi-arch image to forge.quinefoundation.com

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mork Swork 2026-05-14 20:05:46 -07:00
parent d9e6d852d9
commit ed6ff4fbf6
9 changed files with 374 additions and 20 deletions

View file

@ -0,0 +1,33 @@
name: Build and push image
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: forge.quinefoundation.com
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: forge.quinefoundation.com/ironmagma/riverside:latest

View file

@ -1,5 +1,4 @@
#!/bin/sh #!/bin/sh
set -e
DB_HOST="${DB_HOST:-postgres}" DB_HOST="${DB_HOST:-postgres}"
DB_USER="${DB_USER:-drupal}" DB_USER="${DB_USER:-drupal}"
@ -19,39 +18,50 @@ HAS_TABLES=$($DRUSH sql:query \
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public' AND table_name='config';" \ "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public' AND table_name='config';" \
2>/dev/null || echo "0") 2>/dev/null || echo "0")
if [ "$HAS_TABLES" = "1" ]; then IS_SETUP=$($DRUSH sql:query \
echo "[entrypoint] Database populated, importing configuration..." "SELECT COUNT(*) FROM config WHERE name='core.extension' AND data LIKE '%riverside_pt%';" \
$DRUSH config:import -y 2>/dev/null && \ 2>/dev/null || echo "0")
echo "[entrypoint] Config imported." || \
echo "[entrypoint] No config to import, continuing." if [ "$HAS_TABLES" != "1" ]; then
$DRUSH theme:enable claro_compact -y
else
echo "[entrypoint] Fresh database, installing Drupal..." echo "[entrypoint] Fresh database, installing Drupal..."
$DRUSH site:install standard \ $DRUSH site:install standard \
--site-name="${SITE_NAME:-Portfolio}" \ --site-name="${SITE_NAME:-Portfolio}" \
--account-name=admin \ --account-name=admin \
--account-pass="${ADMIN_PASS:-admin}" \ --account-pass="${ADMIN_PASS:-admin}" \
-y -y || { echo "[entrypoint] FATAL: site:install failed."; exit 1; }
echo "[entrypoint] Drupal installed." echo "[entrypoint] Drupal installed."
fi
echo "[entrypoint] Enabling modules..." if [ "$IS_SETUP" != "1" ]; then
$DRUSH en -y views views_ui field_ui text options link datetime echo "[entrypoint] Running setup (first boot or recovery from failed setup)..."
$DRUSH en -y webform webform_ui
$DRUSH en -y symfony_mailer
$DRUSH en -y riverside_pt $DRUSH en -y views views_ui field_ui text options link datetime && \
echo "[entrypoint] Modules enabled." echo "[entrypoint] Core modules enabled." || echo "[entrypoint] WARNING: core modules failed."
$DRUSH en -y webform webform_ui && \
echo "[entrypoint] Webform enabled." || echo "[entrypoint] WARNING: webform failed."
$DRUSH en -y symfony_mailer && \
echo "[entrypoint] Mailer enabled." || echo "[entrypoint] WARNING: symfony_mailer failed."
$DRUSH en -y riverside_pt && \
echo "[entrypoint] riverside_pt enabled." || echo "[entrypoint] WARNING: riverside_pt failed."
echo "[entrypoint] Setting themes..." $DRUSH config:set system.site page.front /home -y && \
$DRUSH theme:enable olivero claro_compact echo "[entrypoint] Front page set." || echo "[entrypoint] WARNING: front page config failed."
echo "[entrypoint] Themes set."
$DRUSH theme:enable olivero claro_compact -y && \
echo "[entrypoint] Themes set." || echo "[entrypoint] WARNING: theme enable failed."
if ls /var/www/html/config/sync/*.yml >/dev/null 2>&1; then if ls /var/www/html/config/sync/*.yml >/dev/null 2>&1; then
echo "[entrypoint] Importing configuration from sync dir..." echo "[entrypoint] Importing configuration from sync dir..."
$DRUSH config:import -y $DRUSH config:import -y || echo "[entrypoint] WARNING: config import failed."
fi
fi fi
echo "[entrypoint] Setup complete."
else
echo "[entrypoint] Setup already complete, importing configuration..."
$DRUSH config:import -y 2>/dev/null && \
echo "[entrypoint] Config imported." || \
echo "[entrypoint] No config to import, continuing."
fi
echo "[entrypoint] Starting services..." echo "[entrypoint] Starting services..."
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf exec supervisord -c /etc/supervisor/conf.d/supervisord.conf

View file

@ -0,0 +1,143 @@
/* Hide Drupal's default page-title block on the front page — our H1 is inside the hero */
.path-frontpage .block-page-title-block {
display: none;
}
/* ── Hero ───────────────────────────────────────────────────── */
.rpt-hero {
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
padding: 5rem 1.5rem;
text-align: center;
}
.rpt-hero__inner {
max-width: 680px;
margin: 0 auto;
}
.rpt-hero__heading {
font-size: clamp(2rem, 5vw, 3.25rem);
font-weight: 700;
color: #1e3a8a;
line-height: 1.15;
margin-bottom: 1rem;
}
.rpt-hero__body {
font-size: 1.125rem;
color: #4b5563;
line-height: 1.7;
margin-bottom: 2rem;
}
.rpt-hero__actions {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
/* ── Shared buttons ─────────────────────────────────────────── */
.rpt-btn {
display: inline-block;
padding: 0.75rem 1.5rem;
border-radius: 999px;
font-weight: 600;
font-size: 0.9375rem;
text-decoration: none;
transition: background 0.15s, color 0.15s, border-color 0.15s;
cursor: pointer;
}
.rpt-btn--primary {
background: #3b82f6;
color: #fff;
border: 2px solid #3b82f6;
}
.rpt-btn--primary:hover,
.rpt-btn--primary:focus {
background: #1d4ed8;
border-color: #1d4ed8;
color: #fff;
}
.rpt-btn--secondary {
background: transparent;
color: #3b82f6;
border: 2px solid #3b82f6;
}
.rpt-btn--secondary:hover,
.rpt-btn--secondary:focus {
background: #3b82f6;
color: #fff;
}
.rpt-btn--outline {
background: transparent;
color: #3b82f6;
border: 1px solid #3b82f6;
border-radius: 4px;
font-size: 0.875rem;
padding: 0.5rem 1rem;
}
.rpt-btn--outline:hover,
.rpt-btn--outline:focus {
background: #3b82f6;
color: #fff;
}
/* ── Services section ───────────────────────────────────────── */
.rpt-services {
padding: 4rem 1.5rem;
background: #fff;
}
.rpt-services__header {
text-align: center;
margin-bottom: 3rem;
}
.rpt-services__heading {
font-size: 2rem;
font-weight: 700;
color: #1e3a8a;
margin-bottom: 0.375rem;
}
.rpt-services__subtitle {
font-size: 1.0625rem;
color: #6b7280;
}
.rpt-services__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.5rem;
max-width: 1040px;
margin: 0 auto;
}
.rpt-service-card {
display: flex;
flex-direction: column;
gap: 0.75rem;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1.5rem;
}
.rpt-service-card__title {
font-size: 1.0625rem;
font-weight: 600;
color: #111827;
}
.rpt-service-card__body {
font-size: 0.9375rem;
color: #6b7280;
line-height: 1.6;
flex: 1;
}

View file

@ -0,0 +1,26 @@
.primary-nav__menu-link.nav-cta {
border: 2px solid #3b82f6;
border-radius: 999px;
color: #3b82f6;
padding: 0.25rem 1rem;
transition: background 0.15s, color 0.15s;
}
.primary-nav__menu-link.nav-cta:hover,
.primary-nav__menu-link.nav-cta:focus {
background: #3b82f6;
border-color: #3b82f6;
color: #fff;
}
.primary-nav__menu-link.nav-cta--primary {
background: #3b82f6;
border-color: #3b82f6;
color: #fff;
}
.primary-nav__menu-link.nav-cta--primary:hover,
.primary-nav__menu-link.nav-cta--primary:focus {
background: #1d4ed8;
border-color: #1d4ed8;
}

View file

@ -1,6 +1,9 @@
<?php <?php
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType; use Drupal\node\Entity\NodeType;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\path_alias\Entity\PathAlias;
use Drupal\user\Entity\Role; use Drupal\user\Entity\Role;
use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldConfig;
@ -140,4 +143,60 @@ function riverside_pt_install() {
'required' => TRUE, 'required' => TRUE,
]), ]),
], fn($entity) => $entity->save()); ], fn($entity) => $entity->save());
try {
_riverside_pt_build_navigation();
}
catch (\Exception $e) {
\Drupal::logger('riverside_pt')->error('Navigation setup failed: @msg', ['@msg' => $e->getMessage()]);
}
\Drupal::configFactory()->getEditable('system.site')
->set('page.front', '/home')
->save();
}
function _riverside_pt_build_navigation(): void {
$em = \Drupal::entityTypeManager();
// Remove whatever links Standard profile put in the main menu.
foreach ($em->getStorage('menu_link_content')->loadByProperties(['menu_name' => 'main']) as $link) {
$link->delete();
}
// Create placeholder basic pages.
foreach (['Services', 'About', 'FAQ', 'Contact'] as $title) {
$existing = $em->getStorage('node')->loadByProperties(['title' => $title, 'type' => 'page']);
if ($existing) {
continue;
}
$node = Node::create(['type' => 'page', 'title' => $title, 'status' => 1]);
$node->save();
PathAlias::create([
'path' => '/node/' . $node->id(),
'alias' => '/' . strtolower($title),
'langcode' => 'en',
])->save();
}
// Build the primary navigation.
$defs = [
['title' => 'Home', 'uri' => 'route:<front>', 'weight' => 0, 'class' => NULL],
['title' => 'Services', 'uri' => 'internal:/services', 'weight' => 1, 'class' => NULL],
['title' => 'About', 'uri' => 'internal:/about', 'weight' => 2, 'class' => NULL],
['title' => 'FAQ', 'uri' => 'internal:/faq', 'weight' => 3, 'class' => NULL],
['title' => 'Contact', 'uri' => 'internal:/contact', 'weight' => 4, 'class' => 'nav-cta'],
['title' => 'Book An Appointment', 'uri' => 'internal:/schedule', 'weight' => 5, 'class' => 'nav-cta nav-cta--primary'],
];
foreach ($defs as $def) {
$options = $def['class'] ? ['attributes' => ['class' => explode(' ', $def['class'])]] : [];
MenuLinkContent::create([
'title' => $def['title'],
'link' => ['uri' => $def['uri'], 'options' => $options],
'menu_name' => 'main',
'weight' => $def['weight'],
'enabled' => TRUE,
])->save();
}
} }

View file

@ -1,3 +1,13 @@
front:
css:
theme:
css/front.css: {}
navigation:
css:
theme:
css/nav.css: {}
schedule: schedule:
css: css:
theme: theme:

View file

@ -4,6 +4,10 @@ use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Link; use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Routing\RouteMatchInterface;
function riverside_pt_page_attachments(array &$attachments): void {
$attachments['#attached']['library'][] = 'riverside_pt/navigation';
}
function riverside_pt_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context): void { function riverside_pt_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context): void {
if ($route_match->getRouteName() === 'riverside_pt.booking') { if ($route_match->getRouteName() === 'riverside_pt.booking') {
$breadcrumb = new Breadcrumb(); $breadcrumb = new Breadcrumb();

View file

@ -1,3 +1,11 @@
riverside_pt.home:
path: '/home'
defaults:
_controller: '\Drupal\riverside_pt\Controller\HomeController::page'
_title: 'Riverside Physical Therapy'
requirements:
_permission: 'access content'
riverside_pt.schedule: riverside_pt.schedule:
path: '/schedule' path: '/schedule'
defaults: defaults:

View file

@ -0,0 +1,61 @@
<?php
namespace Drupal\riverside_pt\Controller;
use Drupal\Core\Controller\ControllerBase;
class HomeController extends ControllerBase {
public function page(): array {
return [
'#type' => 'inline_template',
'#template' => self::template(),
'#attached' => ['library' => ['riverside_pt/front']],
];
}
private static function template(): string {
return <<<'TWIG'
<section class="rpt-hero">
<div class="rpt-hero__inner">
<h1 class="rpt-hero__heading">Heal your body</h1>
<p class="rpt-hero__body">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
<div class="rpt-hero__actions">
<a href="/schedule" class="rpt-btn rpt-btn--primary">Book an Appointment</a>
<a href="/services" class="rpt-btn rpt-btn--secondary">View Our Services</a>
</div>
</div>
</section>
<section class="rpt-services">
<div class="rpt-services__header">
<h2 class="rpt-services__heading">Bringing Relief</h2>
<p class="rpt-services__subtitle">Our wide range of services</p>
</div>
<div class="rpt-services__grid">
<div class="rpt-service-card">
<h3 class="rpt-service-card__title">Diagnostic Evaluation</h3>
<p class="rpt-service-card__body">Comprehensive assessment to identify the root cause of your pain and build a personalized recovery plan.</p>
<a href="/services" class="rpt-btn rpt-btn--outline">More Info</a>
</div>
<div class="rpt-service-card">
<h3 class="rpt-service-card__title">Sports Rehabilitation</h3>
<p class="rpt-service-card__body">Targeted programs to help athletes recover from injury and return to peak performance safely.</p>
<a href="/services" class="rpt-btn rpt-btn--outline">More Info</a>
</div>
<div class="rpt-service-card">
<h3 class="rpt-service-card__title">Pre/Post-Surgical Rehab</h3>
<p class="rpt-service-card__body">Expert care before and after surgery to maximize recovery outcomes and restore full function.</p>
<a href="/services" class="rpt-btn rpt-btn--outline">More Info</a>
</div>
<div class="rpt-service-card">
<h3 class="rpt-service-card__title">Neurological Physical Therapy</h3>
<p class="rpt-service-card__body">Specialized therapy for nervous system conditions, helping you regain strength and independence.</p>
<a href="/services" class="rpt-btn rpt-btn--outline">More Info</a>
</div>
</div>
</section>
TWIG;
}
}