177 lines
6.9 KiB
JavaScript
177 lines
6.9 KiB
JavaScript
|
|
import { getCollection } from "astro:content";
|
||
|
|
|
||
|
|
const siteUrl = (
|
||
|
|
import.meta.env.SITE_URL ||
|
||
|
|
import.meta.env.PUBLIC_SITE_URL ||
|
||
|
|
"https://quietpages-eta.vercel.app"
|
||
|
|
).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",
|
||
|
|
],
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
export const categories = [
|
||
|
|
{ slug: "guides", name: "Guides" },
|
||
|
|
{ slug: "news", name: "News" },
|
||
|
|
{ slug: "reviews", name: "Reviews" },
|
||
|
|
{ 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 imageSrc = (image) => (typeof image === "string" ? image : image?.src);
|
||
|
|
|
||
|
|
// Keep the FULL ISO timestamp (not date-only) so posts published on the same day order by
|
||
|
|
// their real publish time, and so Article schema / sitemap / RSS get precise datePublished.
|
||
|
|
export const normalizePost = (entry) => ({
|
||
|
|
slug: entry.id,
|
||
|
|
...entry.data,
|
||
|
|
date: entry.data.date?.toISOString(),
|
||
|
|
updated: entry.data.updated?.toISOString(),
|
||
|
|
});
|
||
|
|
|
||
|
|
export const posts = async () => (await getCollection("blog")).map(normalizePost);
|
||
|
|
|
||
|
|
export const getPost = async (slug) => (await posts()).find((post) => post.slug === slug);
|
||
|
|
export const getAuthor = (slug) => authors.find((author) => author.slug === slug);
|
||
|
|
export const getCategory = (slug) => categories.find((category) => category.slug === slug);
|
||
|
|
export const getTag = (slug) => tags.find((tag) => tag.slug === slug);
|
||
|
|
export const postsByCategory = async (slug) =>
|
||
|
|
(await sortedPosts()).filter((post) => post.category === slug);
|
||
|
|
export const postsByTag = async (slug) =>
|
||
|
|
(await sortedPosts()).filter((post) => post.tags.includes(slug));
|
||
|
|
export const postsByAuthor = async (slug) =>
|
||
|
|
(await sortedPosts()).filter((post) => post.author === slug);
|
||
|
|
export const sortedPosts = async () =>
|
||
|
|
[...(await posts())].sort((a, b) => {
|
||
|
|
const diff = new Date(b.date) - new Date(a.date); // newest first
|
||
|
|
return diff !== 0 ? diff : a.slug.localeCompare(b.slug); // stable tiebreaker
|
||
|
|
});
|
||
|
|
export const featuredPost = async () => {
|
||
|
|
const sorted = await sortedPosts();
|
||
|
|
return sorted.find((post) => post.featured) ?? sorted[0];
|
||
|
|
};
|
||
|
|
export const popularPosts = async () => (await sortedPosts()).slice(0, 4);
|
||
|
|
export const relatedPosts = async (post, n = 3) =>
|
||
|
|
(await sortedPosts())
|
||
|
|
.filter((candidate) => candidate.slug !== post.slug)
|
||
|
|
.sort((a, b) => {
|
||
|
|
const score = (candidate) =>
|
||
|
|
(candidate.category === post.category ? 2 : 0) +
|
||
|
|
candidate.tags.filter((tag) => post.tags.includes(tag)).length;
|
||
|
|
return score(b) - score(a);
|
||
|
|
})
|
||
|
|
.slice(0, n);
|
||
|
|
|
||
|
|
export const adjacentPosts = async (post) => {
|
||
|
|
const sorted = await sortedPosts();
|
||
|
|
const index = sorted.findIndex((candidate) => candidate.slug === post.slug);
|
||
|
|
return { prev: sorted[index + 1], next: sorted[index - 1] };
|
||
|
|
};
|
||
|
|
|
||
|
|
export const formatDate = (iso) =>
|
||
|
|
new Date(iso).toLocaleDateString("en-US", {
|
||
|
|
year: "numeric",
|
||
|
|
month: "long",
|
||
|
|
day: "numeric",
|
||
|
|
});
|
||
|
|
|
||
|
|
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.",
|
||
|
|
url: siteUrl,
|
||
|
|
};
|
||
|
|
|
||
|
|
// ---- JSON-LD schema builders (no @context — nest these, or add @context at the top level) ----
|
||
|
|
export const organizationSchema = () => ({
|
||
|
|
"@type": "Organization",
|
||
|
|
"@id": `${SITE.url}/#organization`,
|
||
|
|
name: SITE.name,
|
||
|
|
url: `${SITE.url}/`,
|
||
|
|
description: SITE.description,
|
||
|
|
logo: `${SITE.url}/favicon.svg`,
|
||
|
|
});
|
||
|
|
|
||
|
|
export const websiteSchema = () => ({
|
||
|
|
"@type": "WebSite",
|
||
|
|
"@id": `${SITE.url}/#website`,
|
||
|
|
name: SITE.name,
|
||
|
|
url: `${SITE.url}/`,
|
||
|
|
description: SITE.description,
|
||
|
|
inLanguage: "en",
|
||
|
|
publisher: { "@id": `${SITE.url}/#organization` },
|
||
|
|
});
|
||
|
|
|
||
|
|
export const personSchema = (author) =>
|
||
|
|
author && {
|
||
|
|
"@type": "Person",
|
||
|
|
"@id": `${SITE.url}/authors/${author.slug}#person`,
|
||
|
|
name: author.name,
|
||
|
|
url: `${SITE.url}/authors/${author.slug}`,
|
||
|
|
image: author.avatar,
|
||
|
|
description: author.longBio || author.bio,
|
||
|
|
...(author.jobTitle ? { jobTitle: author.jobTitle } : {}),
|
||
|
|
...(author.knowsAbout ? { knowsAbout: author.knowsAbout } : {}),
|
||
|
|
...(author.sameAs ? { sameAs: author.sameAs } : {}),
|
||
|
|
};
|