seedproject-web/app/src/pages/index.astro
Carlos Arias fc84d095b8 feat: config-driven identity (site.config.json) + buildable sample content
- site.config.json  single source of identity (name/url/description/author/social)
- scripts/configure.mjs  stamps it into app, content-pipeline, and astroagent configs
- app/src/lib/blog-data.js  SITE + author now read from app/src/config/site.json
- astro.config.mjs  default site URL from config
- index.astro / Footer.astro  neutralized hardcoded title, hero alt, social link
- sample 'welcome' post + placeholder hero/avatar so the site builds out of the box

Verified: cd app && npm ci && npm run build -> 13 pages, Complete.
Remaining comiida demo prose in about/faq/services/contact + Newsletter (sample content).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYHWLHihq3v9nxNwoPCKSn
2026-07-04 23:06:23 +00:00

225 lines
8.9 KiB
Text

---
import { Image } from "astro:assets";
import BaseLayout from "../layouts/BaseLayout.astro";
import PostCard from "../components/PostCard.astro";
import Newsletter from "../components/Newsletter.astro";
import Icon from "../components/Icon.astro";
import heroImage from "../assets/hero-placeholder.png";
import {
SITE,
categories,
featuredPost,
sortedPosts,
getAuthor,
getCategory,
formatDate,
organizationSchema,
websiteSchema,
} from "../lib/blog-data.js";
const featured = await featuredPost();
const featuredAuthor = getAuthor(featured.author);
const featuredCategory = getCategory(featured.category);
const latest = (await sortedPosts()).filter((post) => post.slug !== featured.slug);
const jsonLd = {
"@context": "https://schema.org",
"@graph": [organizationSchema(), websiteSchema()],
};
---
<BaseLayout
title={`${SITE.name} — ${SITE.tagline}`}
description={SITE.description}
canonical="/"
jsonLd={jsonLd}
flushFooter
>
<section class="relative -mt-[calc(4rem+2px)] min-h-[calc(100svh-2rem)] overflow-hidden border-b border-border/60 pt-[calc(4rem+2px)] sm:min-h-[760px]">
<Image
src={heroImage}
alt="Hero image placeholder — replace in app/src/assets"
class="absolute inset-0 h-full w-full object-cover object-center"
loading="eager"
decoding="async"
widths={[640, 960, 1280, 1920, 2560]}
sizes="100vw"
/>
<div
aria-hidden="true"
class="absolute inset-0 bg-[linear-gradient(90deg,oklch(0.1_0.02_70_/_0.68),oklch(0.16_0.02_80_/_0.38)_46%,oklch(0.98_0.01_90_/_0.1))] dark:bg-[linear-gradient(90deg,oklch(0.08_0.01_70_/_0.78),oklch(0.08_0.01_70_/_0.52)_48%,oklch(0.08_0.01_70_/_0.18))]"
/>
<div
aria-hidden="true"
class="absolute inset-x-0 top-0 h-40 bg-gradient-to-b from-black/45 via-black/18 to-transparent"
/>
<div
aria-hidden="true"
class="absolute inset-x-0 bottom-0 h-48 bg-gradient-to-t from-background via-background/72 to-transparent"
/>
<div class="relative mx-auto flex min-h-[calc(100svh-6rem)] max-w-6xl items-center px-5 py-20 sm:min-h-[680px] sm:py-24">
<div class="max-w-4xl text-white drop-shadow-[0_1px_18px_rgba(0,0,0,0.28)]">
<h1 class="font-serif text-[2.5rem] font-medium leading-[1.02] tracking-[-0.02em] sm:text-6xl lg:text-7xl">
Medellín&rsquo;s
<span class="italic text-white"> restaurants</span>,
<span class="italic text-white"> food news</span>, and
<span class="italic text-white"> where to eat</span>.
</h1>
<p class="mt-8 max-w-xl text-lg leading-relaxed text-white/86">
Guides, openings, and data-driven dining picks for English-speaking expats, nomads, and travelers &mdash; researched carefully and updated often.
</p>
<div class="mt-10 flex flex-wrap items-center gap-x-8 gap-y-4">
<a
href={`/blog/${featured.slug}`}
class="group inline-flex items-center gap-2 border-b border-white pb-1 text-sm font-medium text-white hover:border-white/70 hover:text-white/82"
>
Begin with this issue's lead essay
<Icon name="arrow-right" class="h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" />
</a>
<a href="/blog" class="text-sm text-white/76 hover:text-white">
Or browse the archive
</a>
</div>
</div>
</div>
</section>
<section class="border-b border-border/60">
<div class="mx-auto max-w-6xl px-5 py-16">
<div class="mb-6 flex items-center justify-between">
<div class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Featured</div>
<a href="/blog" class="text-xs text-muted-foreground hover:text-foreground">All writing &rarr;</a>
</div>
<article class="grid gap-10 lg:grid-cols-2">
<a href={`/blog/${featured.slug}`} class="group block" aria-label={`Read ${featured.title}`}>
<div class="aspect-[4/3] overflow-hidden rounded-md bg-muted">
{
featured.thumbnail && (
<Image
src={featured.thumbnail}
alt=""
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.02]"
/>
)
}
</div>
</a>
<div class="flex flex-col justify-center">
<div class="flex items-center gap-3 text-xs uppercase tracking-wider text-muted-foreground">
{
featuredCategory && (
<a href={`/categories/${featuredCategory.slug}`} class="text-primary hover:underline">
{featuredCategory.name}
</a>
)
}
<span>&middot;</span>
<time datetime={featured.date}>{formatDate(featured.date)}</time>
</div>
<a href={`/blog/${featured.slug}`}>
<h2 class="mt-3 font-serif text-3xl font-semibold leading-tight tracking-tight hover:text-primary sm:text-4xl">
{featured.title}
</h2>
</a>
<p class="mt-4 text-lg text-muted-foreground">{featured.excerpt}</p>
<div class="mt-6 flex items-center gap-3 text-sm text-muted-foreground">
{
featuredAuthor && (
<>
<img
src={featuredAuthor.avatar}
alt=""
width="28"
height="28"
class="h-7 w-7 rounded-full"
/>
<a href={`/authors/${featuredAuthor.slug}`} class="hover:text-foreground">{featuredAuthor.name}</a>
<span>&middot;</span>
</>
)
}
<span>{featured.readingTime} min read</span>
</div>
<a href={`/blog/${featured.slug}`} class="mt-6 inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline">
Read the essay <Icon name="arrow-right" class="h-3.5 w-3.5" />
</a>
</div>
</article>
</div>
</section>
<section class="mx-auto max-w-6xl px-5 py-16" data-home-latest>
<div class="flex flex-wrap items-center justify-between gap-4">
<h2 class="font-serif text-2xl font-semibold tracking-tight">Latest</h2>
<div class="flex flex-wrap gap-2">
<button
type="button"
data-home-filter=""
class="rounded-full border px-3 py-1 text-xs transition-colors border-foreground bg-foreground text-background"
>
All
</button>
{
categories.map((category) => (
<button
type="button"
data-home-filter={category.slug}
class="rounded-full border px-3 py-1 text-xs transition-colors border-border text-muted-foreground hover:text-foreground"
>
{category.name}
</button>
))
}
</div>
</div>
<div data-home-empty class="mt-12 rounded-md border border-dashed border-border py-16 text-center text-muted-foreground" hidden>
Nothing here yet in this section.
</div>
<div class="mt-10 grid gap-10 sm:grid-cols-2 lg:grid-cols-3">
{latest.map((post, index) => <PostCard post={post} hidden={index >= 6} />)}
</div>
<div class="mt-12 text-center">
<a href="/blog" class="inline-flex items-center gap-2 rounded-md border border-border px-5 py-2.5 text-sm font-medium hover:bg-muted">
Browse the archive <Icon name="arrow-right" class="h-3.5 w-3.5" />
</a>
</div>
</section>
<Newsletter />
</BaseLayout>
<script>
const latest = document.querySelector("[data-home-latest]");
if (latest) {
const buttons = [...latest.querySelectorAll("[data-home-filter]")];
const cards = [...latest.querySelectorAll("[data-post-card]")];
const empty = latest.querySelector("[data-home-empty]");
const activeClasses = ["border-foreground", "bg-foreground", "text-background"];
const inactiveClasses = ["border-border", "text-muted-foreground", "hover:text-foreground"];
const setActive = (button, active) => {
button.classList.toggle(activeClasses[0], active);
button.classList.toggle(activeClasses[1], active);
button.classList.toggle(activeClasses[2], active);
inactiveClasses.forEach((className) => button.classList.toggle(className, !active));
};
const filterCards = (category) => {
let visible = 0;
cards.forEach((card) => {
const match = !category || card.dataset.category === category;
const show = match && visible < 6;
card.hidden = !show;
if (match) visible += 1;
});
if (empty) empty.hidden = visible > 0;
buttons.forEach((button) => setActive(button, button.dataset.homeFilter === category));
};
buttons.forEach((button) => {
button.addEventListener("click", () => filterCards(button.dataset.homeFilter || ""));
});
}
</script>