Static marketing site for G&G Ingeniería SPA. Built with Astro and Tailwind CSS. Services and their photos are pulled from Supabase at build time, so the published site is plain HTML with no client-side data fetching.
- Node.js 20 or newer
npm install
# create .env with the variables listed below
npm run dev # http://localhost:4321Create a .env file in the project root. None of these use the PUBLIC_
prefix, which keeps them server-side only — they are read in Astro frontmatter
during the build and never reach the browser.
| Variable | Description |
|---|---|
SUPABASE_URL |
Supabase project URL. |
SUPABASE_PUBLISHABLE_KEY |
Supabase publishable (anon) key. Read-only. |
IMAGE_BASE_URL |
Public domain of the Cloudflare R2 bucket, without a trailing slash. |
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxx
IMAGE_BASE_URL=https://images.example.com| Command | Description |
|---|---|
npm run dev |
Dev server on localhost:4321. |
npm run build |
Static build into dist/. |
npm run preview |
Serve the production build locally. |
Services are managed from a separate admin. This site only reads them.
src/lib/supabase.ts— the Supabase client, created once and reused. Returnsnullwhen the environment is not configured.src/lib/services.ts—getServices()fetches published services with their media, sorts each gallery byposition, and resolves R2 URLs. It never throws: on error it logs a warning and returns[], so the services section falls back to a CTA instead of breaking the build.src/lib/media.ts— the single read path intopublic.media.getGeneralMedia()returns the landing's general assets (service_id IS NULL) for the "Nuestro trabajo" section, andmediaUrl(path)joinsIMAGE_BASE_URLwith the object key stored inmedia.path. Keys are relative on purpose so the bucket domain can change without touching the database.src/lib/site.ts— phone, email, hours and the hero figures. Edit contact details and headline stats here; nothing else hardcodes them.
services id, slug, name, description, position, is_published, created_at, updated_at
media id, service_id -> services.id (NULL = general), kind ('image'|'video'),
path, alt, description, position, created_at
Two rules carry the meaning of media:
service_id IS NULLmeans the asset is general: it belongs to the landing as a whole, not to a service. There is no boolean flag.kinddistinguishes image from video. Never infer it from the file extension.
alt is accessibility text; description is the visible caption. They are not
interchangeable, and both are nullable.
Anonymous access can read all general media plus the media of published
services; RLS handles that filter, so no is_published check is needed on media
rows. Every write requires an authenticated user; this site never writes.
The service_images view is a frozen backward-compatibility shim over
media WHERE kind = 'image' AND service_id IS NOT NULL. Nothing here reads it.
Publish it from the admin, then redeploy. Content is baked in at build time, so a rebuild is what makes new services appear.
Tokens live in src/styles/global.css under @theme (Tailwind v4 is
CSS-first — there is no tailwind.config.js). The palette keeps the original
brand colours: deep navy #0a263b (ink-900), steel #3b566e (steel-500)
and coral #f65757 (accent-500).
<img loading="lazy"> is used directly for remote R2 images. If you switch to
<Image /> from astro:assets, declare the bucket host in
image.remotePatterns inside astro.config.mjs first.