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:
parent
1559ce017d
commit
fc84d095b8
11 changed files with 183 additions and 88 deletions
|
|
@ -1,9 +1,14 @@
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
import { defineConfig } from "astro/config";
|
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 mdx from "@astrojs/mdx";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
const site =
|
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({
|
export default defineConfig({
|
||||||
site,
|
site,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ const { flush = false } = Astro.props;
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-4 flex items-center gap-3 text-muted-foreground">
|
<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="/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>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
20
app/src/config/site.json
Normal file
20
app/src/config/site.json
Normal 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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
app/src/content/blog/welcome/cover.png
Normal file
BIN
app/src/content/blog/welcome/cover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5 KiB |
22
app/src/content/blog/welcome/index.mdx
Normal file
22
app/src/content/blog/welcome/index.mdx
Normal 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`.
|
||||||
|
|
@ -1,33 +1,22 @@
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
|
import siteConfig from "../config/site.json";
|
||||||
|
|
||||||
const siteUrl = (
|
const siteUrl = (
|
||||||
import.meta.env.SITE_URL ||
|
import.meta.env.SITE_URL ||
|
||||||
import.meta.env.PUBLIC_SITE_URL ||
|
import.meta.env.PUBLIC_SITE_URL ||
|
||||||
"https://quietpages-eta.vercel.app"
|
siteConfig.url
|
||||||
).replace(/\/$/, "");
|
).replace(/\/$/, "");
|
||||||
|
|
||||||
export const authors = [
|
export const authors = [
|
||||||
{
|
{
|
||||||
slug: "carlos-arias",
|
slug: siteConfig.author.slug,
|
||||||
name: "Carlos Arias",
|
name: siteConfig.author.name,
|
||||||
bio: "AI engineer and digital strategist with 25+ years building software and AI systems; founder of CarlosArias&Co and engineer behind Medellín.co.",
|
bio: siteConfig.author.bio,
|
||||||
longBio:
|
longBio: siteConfig.author.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: siteConfig.author.avatar,
|
||||||
avatar: "https://carlosarias.com/wp-content/uploads/2023/02/cropped-clos.jpg",
|
jobTitle: siteConfig.author.jobTitle,
|
||||||
jobTitle: "Founder & Editor, Comiida",
|
knowsAbout: siteConfig.author.knowsAbout || [],
|
||||||
knowsAbout: [
|
sameAs: siteConfig.author.sameAs || [],
|
||||||
"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",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -38,45 +27,7 @@ export const categories = [
|
||||||
{ slug: "neighborhoods", name: "Neighborhoods" },
|
{ slug: "neighborhoods", name: "Neighborhoods" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const tags = [
|
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 imageSrc = (image) => (typeof image === "string" ? image : image?.src);
|
export const imageSrc = (image) => (typeof image === "string" ? image : image?.src);
|
||||||
|
|
||||||
|
|
@ -136,9 +87,9 @@ export const formatDate = (iso) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SITE = {
|
export const SITE = {
|
||||||
name: "Comiida",
|
name: siteConfig.name,
|
||||||
description:
|
description: siteConfig.description,
|
||||||
"Medellín's restaurant scene for English-speaking expats, nomads, and travelers — guides, news, and data-driven dining picks.",
|
tagline: siteConfig.tagline,
|
||||||
url: siteUrl,
|
url: siteUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ const jsonLd = {
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
title={`${SITE.name} - Medellín's restaurant scene for expats, nomads & travelers`}
|
title={`${SITE.name} — ${SITE.tagline}`}
|
||||||
description={SITE.description}
|
description={SITE.description}
|
||||||
canonical="/"
|
canonical="/"
|
||||||
jsonLd={jsonLd}
|
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]">
|
<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
|
<Image
|
||||||
src={heroImage}
|
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"
|
class="absolute inset-0 h-full w-full object-cover object-center"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
|
|
@ -125,11 +125,11 @@ const jsonLd = {
|
||||||
{
|
{
|
||||||
featuredAuthor && (
|
featuredAuthor && (
|
||||||
<>
|
<>
|
||||||
<Image
|
<img
|
||||||
src={featuredAuthor.avatar}
|
src={featuredAuthor.avatar}
|
||||||
alt=""
|
alt=""
|
||||||
width={28}
|
width="28"
|
||||||
height={28}
|
height="28"
|
||||||
class="h-7 w-7 rounded-full"
|
class="h-7 w-7 rounded-full"
|
||||||
/>
|
/>
|
||||||
<a href={`/authors/${featuredAuthor.slug}`} class="hover:text-foreground">{featuredAuthor.name}</a>
|
<a href={`/authors/${featuredAuthor.slug}`} class="hover:text-foreground">{featuredAuthor.name}</a>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "comiida",
|
"name": "seedproject-site",
|
||||||
"url": "https://comiida.com",
|
"url": "https://example.com",
|
||||||
"appDir": "app",
|
"appDir": "app",
|
||||||
"outDir": "public",
|
"outDir": "public",
|
||||||
"buildCommand": "npm run build",
|
"buildCommand": "npm run build",
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"site": {
|
"site": {
|
||||||
"url": "https://comiida.com",
|
"url": "https://example.com",
|
||||||
"name": "Comiida",
|
"name": "SeedProject Site",
|
||||||
"topic": "the restaurant industry in Medellin, Colombia",
|
"topic": "your subject area",
|
||||||
"audience": "English-speaking expats, digital nomads, and tourists in Medellin",
|
"audience": "your target audience",
|
||||||
"language": "en"
|
"language": "en"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|
@ -36,8 +36,8 @@
|
||||||
"maxPerScan": 8
|
"maxPerScan": 8
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
"slug": "carlos-arias",
|
"slug": "site-author",
|
||||||
"name": "Carlos Arias"
|
"name": "Site Author"
|
||||||
},
|
},
|
||||||
"image": {
|
"image": {
|
||||||
"provider": "higgsfield",
|
"provider": "higgsfield",
|
||||||
|
|
@ -46,27 +46,42 @@
|
||||||
"outputFormat": "jpg",
|
"outputFormat": "jpg",
|
||||||
"credit": {
|
"credit": {
|
||||||
"caption": "Illustrative cover image. Not a photograph of any specific establishment.",
|
"caption": "Illustrative cover image. Not a photograph of any specific establishment.",
|
||||||
"author": "Comiida"
|
"author": "SeedProject Site"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"editorial": {
|
"editorial": {
|
||||||
"calendarDays": 90,
|
"calendarDays": 90,
|
||||||
"refillThreshold": 7,
|
"refillThreshold": 7,
|
||||||
"refillCount": 14,
|
"refillCount": 14,
|
||||||
"timezone": "America/Bogota",
|
"timezone": "America/New_York",
|
||||||
"contentMix": {
|
"contentMix": {
|
||||||
"guide": 0.30,
|
"guide": 0.3,
|
||||||
"list": 0.25,
|
"list": 0.25,
|
||||||
"news-roundup": 0.20,
|
"news-roundup": 0.2,
|
||||||
"trend": 0.15,
|
"trend": 0.15,
|
||||||
"review-summary": 0.10
|
"review-summary": 0.1
|
||||||
},
|
},
|
||||||
"wordCounts": {
|
"wordCounts": {
|
||||||
"news-roundup": [600, 900],
|
"news-roundup": [
|
||||||
"trend": [800, 1200],
|
600,
|
||||||
"review-summary": [900, 1300],
|
900
|
||||||
"list": [1000, 1500],
|
],
|
||||||
"guide": [1200, 1800]
|
"trend": [
|
||||||
|
800,
|
||||||
|
1200
|
||||||
|
],
|
||||||
|
"review-summary": [
|
||||||
|
900,
|
||||||
|
1300
|
||||||
|
],
|
||||||
|
"list": [
|
||||||
|
1000,
|
||||||
|
1500
|
||||||
|
],
|
||||||
|
"guide": [
|
||||||
|
1200,
|
||||||
|
1800
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
59
scripts/configure.mjs
Normal file
59
scripts/configure.mjs
Normal 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
23
site.config.json
Normal 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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue