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";
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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
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 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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
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