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
This commit is contained in:
Carlos Arias 2026-07-04 23:06:23 +00:00
parent 1559ce017d
commit fc84d095b8
11 changed files with 183 additions and 88 deletions

View file

@ -1,9 +1,14 @@
import { readFileSync } from "node:fs";
import { defineConfig } from "astro/config";
const siteConfig = JSON.parse(
readFileSync(new URL("./src/config/site.json", import.meta.url)),
);
import mdx from "@astrojs/mdx";
import tailwindcss from "@tailwindcss/vite";
const site =
process.env.SITE_URL || process.env.PUBLIC_SITE_URL || "https://comiida.com";
process.env.SITE_URL || process.env.PUBLIC_SITE_URL || siteConfig.url;
export default defineConfig({
site,

View file

@ -14,7 +14,7 @@ const { flush = false } = Astro.props;
</p>
<div class="mt-4 flex items-center gap-3 text-muted-foreground">
<a href="/rss.xml" aria-label="RSS" class="hover:text-foreground"><Icon name="rss" class="h-4 w-4" /></a>
<a href="https://x.com/hicarlosarias" aria-label="Comiida on X" class="hover:text-foreground"><Icon name="twitter" class="h-4 w-4" /></a>
<a href="https://x.com/" aria-label={`${SITE.name} on X`} class="hover:text-foreground"><Icon name="twitter" class="h-4 w-4" /></a>
</div>
</div>
<div>

20
app/src/config/site.json Normal file
View file

@ -0,0 +1,20 @@
{
"name": "SeedProject Site",
"url": "https://example.com",
"description": "A short one-line description of this site, used for SEO and social sharing.",
"tagline": "A starter site built on SeedProject",
"language": "en",
"author": {
"slug": "site-author",
"name": "Site Author",
"bio": "One-line author bio — who runs this site.",
"longBio": "A longer author biography paragraph describing who runs this site and their background.",
"avatar": "/avatar-placeholder.png",
"jobTitle": "Founder & Editor",
"knowsAbout": [],
"sameAs": []
},
"social": {
"twitter": ""
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

View file

@ -0,0 +1,22 @@
---
title: "Welcome to your new site"
excerpt: "A sample post so your blog builds out of the box. Edit or delete it once you add real content."
date: 2026-01-01
readingTime: 2
category: "guides"
author: "site-author"
thumbnail: "./cover.png"
featured: true
---
This is a sample post included with the **SeedProject base**. It exists so the
blog, homepage, RSS feed, and sitemap all build before you've written anything.
## Getting started
1. Edit `site.config.json` at the repo root with your site's name, URL, and author.
2. Run `node scripts/configure.mjs` to stamp that identity across the theme and engines.
3. Replace this post with your own content under `app/src/content/blog/`.
Each post is a folder with an `index.mdx` file and a `cover` image, using the
frontmatter fields defined in `app/src/content.config.js`.

View file

@ -1,33 +1,22 @@
import { getCollection } from "astro:content";
import siteConfig from "../config/site.json";
const siteUrl = (
import.meta.env.SITE_URL ||
import.meta.env.PUBLIC_SITE_URL ||
"https://quietpages-eta.vercel.app"
siteConfig.url
).replace(/\/$/, "");
export const authors = [
{
slug: "carlos-arias",
name: "Carlos Arias",
bio: "AI engineer and digital strategist with 25+ years building software and AI systems; founder of CarlosArias&Co and engineer behind Medellín.co.",
longBio:
"Carlos Arias is an AI engineer and strategist with over two decades across software engineering, digital marketing, and AI. He is the founder of CarlosArias&Co and Snoopi.io, and an engineer and strategist behind the Medellín.co city guide. Based between South Florida and Latin America, he builds AI-driven systems for local visibility and revenue-tracked growth. Comiida's articles are researched from public sources and AI-assisted under his editorial oversight.",
avatar: "https://carlosarias.com/wp-content/uploads/2023/02/cropped-clos.jpg",
jobTitle: "Founder & Editor, Comiida",
knowsAbout: [
"Medellín restaurants",
"Colombian cuisine",
"dining guides",
"local SEO",
],
sameAs: [
"https://x.com/hicarlosarias",
"https://www.linkedin.com/in/hicarlosarias/",
"https://www.instagram.com/carlosarias.co",
"https://www.facebook.com/hicarlosarias",
"https://carlosarias.com",
],
slug: siteConfig.author.slug,
name: siteConfig.author.name,
bio: siteConfig.author.bio,
longBio: siteConfig.author.longBio,
avatar: siteConfig.author.avatar,
jobTitle: siteConfig.author.jobTitle,
knowsAbout: siteConfig.author.knowsAbout || [],
sameAs: siteConfig.author.sameAs || [],
},
];
@ -38,45 +27,7 @@ export const categories = [
{ slug: "neighborhoods", name: "Neighborhoods" },
];
export const tags = [
{ slug: "food-guide", name: "Food Guide" },
{ slug: "ciudad-del-rio", name: "Ciudad Del Rio" },
{ slug: "things-to-do", name: "Things To Do" },
{ slug: "terminal-del-sur", name: "Terminal Del Sur" },
{ slug: "specialty-coffee", name: "Specialty Coffee" },
{ slug: "craft-beer", name: "Craft Beer" },
{ slug: "rionegro", name: "Rionegro" },
{ slug: "antioquia", name: "Antioquia" },
{ slug: "day-trip", name: "Day Trip" },
{ slug: "oriente-antioqueno", name: "Oriente Antioqueno" },
{ slug: "food-festival", name: "Food Festival" },
{ slug: "marinilla", name: "Marinilla" },
{ slug: "wake-medellin", name: "Wake Medellin" },
{ slug: "peruvian-cuisine", name: "Peruvian Cuisine" },
{ slug: "steakhouse", name: "Steakhouse" },
{ slug: "gastronomy", name: "Gastronomy" },
{ slug: "time-out", name: "Time Out" },
{ slug: "food-ranking", name: "Food Ranking" },
{ slug: "chef", name: "Chef" },
{ slug: "colombian-cuisine", name: "Colombian Cuisine" },
{ slug: "new-restaurant", name: "New Restaurant" },
{ slug: "legal-news", name: "Legal News" },
{ slug: "fine-dining", name: "Fine Dining" },
{ slug: "hidden-kitchen", name: "Hidden Kitchen" },
{ slug: "chefs-table", name: "Chefs Table" },
{ slug: "tasting-menu", name: "Tasting Menu" },
{ slug: "new-openings", name: "New Openings" },
{ slug: "restaurant-news", name: "Restaurant News" },
{ slug: "restaurants", name: "Restaurants" },
{ slug: "coffee", name: "Coffee" },
{ slug: "el-poblado", name: "El Poblado" },
{ slug: "brunch", name: "Brunch" },
{ slug: "expats", name: "Expats" },
{ slug: "food-safety", name: "Food Safety" },
{ slug: "street-food", name: "Street Food" },
{ slug: "medellin", name: "Medellín" },
{ slug: "travel", name: "Travel" },
];
export const tags = [];
export const imageSrc = (image) => (typeof image === "string" ? image : image?.src);
@ -136,9 +87,9 @@ export const formatDate = (iso) =>
});
export const SITE = {
name: "Comiida",
description:
"Medellín's restaurant scene for English-speaking expats, nomads, and travelers — guides, news, and data-driven dining picks.",
name: siteConfig.name,
description: siteConfig.description,
tagline: siteConfig.tagline,
url: siteUrl,
};

View file

@ -28,7 +28,7 @@ const jsonLd = {
---
<BaseLayout
title={`${SITE.name} - Medellín's restaurant scene for expats, nomads & travelers`}
title={`${SITE.name} — ${SITE.tagline}`}
description={SITE.description}
canonical="/"
jsonLd={jsonLd}
@ -37,7 +37,7 @@ const jsonLd = {
<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="Watercolor painting of Medellín's green Andean mountains and valley"
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"
@ -125,11 +125,11 @@ const jsonLd = {
{
featuredAuthor && (
<>
<Image
<img
src={featuredAuthor.avatar}
alt=""
width={28}
height={28}
width="28"
height="28"
class="h-7 w-7 rounded-full"
/>
<a href={`/authors/${featuredAuthor.slug}`} class="hover:text-foreground">{featuredAuthor.name}</a>

View file

@ -1,6 +1,6 @@
{
"name": "comiida",
"url": "https://comiida.com",
"name": "seedproject-site",
"url": "https://example.com",
"appDir": "app",
"outDir": "public",
"buildCommand": "npm run build",

View file

@ -1,9 +1,9 @@
{
"site": {
"url": "https://comiida.com",
"name": "Comiida",
"topic": "the restaurant industry in Medellin, Colombia",
"audience": "English-speaking expats, digital nomads, and tourists in Medellin",
"url": "https://example.com",
"name": "SeedProject Site",
"topic": "your subject area",
"audience": "your target audience",
"language": "en"
},
"paths": {
@ -36,8 +36,8 @@
"maxPerScan": 8
},
"author": {
"slug": "carlos-arias",
"name": "Carlos Arias"
"slug": "site-author",
"name": "Site Author"
},
"image": {
"provider": "higgsfield",
@ -46,27 +46,42 @@
"outputFormat": "jpg",
"credit": {
"caption": "Illustrative cover image. Not a photograph of any specific establishment.",
"author": "Comiida"
"author": "SeedProject Site"
}
},
"editorial": {
"calendarDays": 90,
"refillThreshold": 7,
"refillCount": 14,
"timezone": "America/Bogota",
"timezone": "America/New_York",
"contentMix": {
"guide": 0.30,
"guide": 0.3,
"list": 0.25,
"news-roundup": 0.20,
"news-roundup": 0.2,
"trend": 0.15,
"review-summary": 0.10
"review-summary": 0.1
},
"wordCounts": {
"news-roundup": [600, 900],
"trend": [800, 1200],
"review-summary": [900, 1300],
"list": [1000, 1500],
"guide": [1200, 1800]
"news-roundup": [
600,
900
],
"trend": [
800,
1200
],
"review-summary": [
900,
1300
],
"list": [
1000,
1500
],
"guide": [
1200,
1800
]
}
}
}

59
scripts/configure.mjs Normal file
View file

@ -0,0 +1,59 @@
#!/usr/bin/env node
/**
* configure.mjs stamp site.config.json (the single source of identity) into
* every engine config. Run after editing site.config.json:
*
* node scripts/configure.mjs
*
* Writes:
* - app/src/config/site.json (Astro theme reads this)
* - content-pipeline/config.json (site identity + author + timezone)
* - astroagent.config.json (name + url)
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const read = (p) => JSON.parse(readFileSync(resolve(root, p), "utf8"));
const write = (p, obj) => {
const full = resolve(root, p);
mkdirSync(dirname(full), { recursive: true });
writeFileSync(full, JSON.stringify(obj, null, 2) + "\n");
console.log(" wrote", p);
};
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
const site = read("site.config.json");
console.log(`Configuring site: ${site.name} <${site.url}>`);
// 1) Astro theme
write("app/src/config/site.json", {
name: site.name,
url: site.url,
description: site.description,
tagline: site.tagline,
language: site.language,
author: site.author,
social: site.social,
});
// 2) content-pipeline
if (existsSync(resolve(root, "content-pipeline/config.json"))) {
const cp = read("content-pipeline/config.json");
cp.site = { ...cp.site, url: site.url, name: site.name, topic: site.topic, audience: site.audience, language: site.language };
cp.author = { ...(cp.author || {}), slug: site.author.slug, name: site.author.name };
if (cp.editorial) cp.editorial.timezone = site.timezone;
if (cp.image && cp.image.credit) cp.image.credit.author = site.name;
write("content-pipeline/config.json", cp);
}
// 3) astroagent
if (existsSync(resolve(root, "astroagent.config.json"))) {
const aa = read("astroagent.config.json");
aa.name = slug(site.name);
aa.url = site.url;
write("astroagent.config.json", aa);
}
console.log("Done. Rebuild the site: (cd app && npm run build)");

23
site.config.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "SeedProject Site",
"url": "https://example.com",
"description": "A short one-line description of this site, used for SEO and social sharing.",
"tagline": "A starter site built on SeedProject",
"language": "en",
"timezone": "America/New_York",
"topic": "your subject area",
"audience": "your target audience",
"author": {
"slug": "site-author",
"name": "Site Author",
"bio": "One-line author bio — who runs this site.",
"longBio": "A longer author biography paragraph describing who runs this site and their background.",
"avatar": "/avatar-placeholder.png",
"jobTitle": "Founder & Editor",
"knowsAbout": [],
"sameAs": []
},
"social": {
"twitter": ""
}
}