Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
14 changes: 14 additions & 0 deletions config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Site Configuration - Control visibility of sections
sections:
about: true
workExperience: true
talks: true
writing: true
socialLinks: true

# Individual elements
elements:
avatar: true
themeSwitch: true
header: true
footer: true
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
"@astrojs/sitemap": "3.6.0",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.16",
"@types/js-yaml": "^4.0.9",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"astro": "5.15.2",
"framer-motion": "^12.23.24",
"js-yaml": "^4.1.0",
"lucide-react": "^0.548.0",
"mdast-util-to-string": "^4.0.0",
"react": "^18.3.1",
Expand Down
15 changes: 10 additions & 5 deletions src/components/partials/Footer.astro
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
---
import Container from '@/components/Container.astro';
import { getSiteConfig } from '@/lib/config';

const config = getSiteConfig();
---

<Container as='footer' class='pt-24'>
<p class="text-center text-muted-foreground text-sm">
&copy; {new Date().getFullYear()}. Powered by <a href="https://astro.build" target="_blank" rel="noopener noreferrer">Astro</a> and CVfolio.
</p>
</Container>
{config.elements.footer && (
<Container as='footer' class='pt-24'>
<p class="text-center text-muted-foreground text-sm">
&copy; {new Date().getFullYear()}. Powered by <a href="https://astro.build" target="_blank" rel="noopener noreferrer">Astro</a> and CVfolio.
</p>
</Container>
)}
74 changes: 40 additions & 34 deletions src/components/partials/Header.astro
Original file line number Diff line number Diff line change
@@ -1,43 +1,49 @@
---
import Container from '@/components/Container.astro';
import { getSiteConfig } from '@/lib/config';

const pathname = Astro.url.pathname;

const isHomePage = pathname === '/';
const isWritingPage = pathname.startsWith('/writing');
---

<Container
as="header"
class="w-full max-w-full flex justify-center items-center"
>
<div
class="w-max fixed top-0 mt-5 bg-muted-foreground/40 backdrop-blur-3xl border border-border rounded-full p-1"
const config = getSiteConfig();
---
{config.elements.header && (
<Container
as="header"
class="w-full max-w-full flex justify-center items-center"
>
<nav class="flex items-center">
<ul class="flex items-center gap-1">
<li>
<a
href="/"
class:list={[
'font-medium transition-colors block px-5 py-2',
'hover:text-headings',
isHomePage && 'text-headings bg-muted-foreground/40 rounded-full',
]}>Home</a
>
</li>
<li>
<a
href="/writing"
class:list={[
'font-medium transition-colors block px-5 py-2',
'hover:text-headings',
isWritingPage &&
'text-headings bg-muted-foreground/40 rounded-full',
]}>Writing</a
>
</li>
</ul>
</nav>
</div>
</Container>
<div
class="w-max fixed top-0 mt-5 bg-muted-foreground/40 backdrop-blur-3xl border border-border rounded-full p-1"
>
<nav class="flex items-center">
<ul class="flex items-center gap-1">
<li>
<a
href="/"
class:list={[
'font-medium transition-colors block px-5 py-2',
'hover:text-headings',
isHomePage && 'text-headings bg-muted-foreground/40 rounded-full',
]}>Home</a
>
</li>
{config.sections.writing && (
<li>
<a
href="/writing"
class:list={[
'font-medium transition-colors block px-5 py-2',
'hover:text-headings',
isWritingPage &&
'text-headings bg-muted-foreground/40 rounded-full',
]}>Writing</a
>
</li>
)}
</ul>
</nav>
</div>
</Container>
)}
6 changes: 5 additions & 1 deletion src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import Header from '@/components/partials/Header.astro';
import Footer from '@/components/partials/Footer.astro';
import Head from '@/components/partials/Head.astro';
import SwitchTheme from '@/components/SwitchTheme.tsx';
import { getSiteConfig } from '@/lib/config';

interface Props {
seo?: Seo
}

const { seo } = Astro.props;
const config = getSiteConfig();
---

<html lang="en">
Expand All @@ -22,6 +24,8 @@ const { seo } = Astro.props;
<slot />
</main>
<Footer />
<SwitchTheme client:only="react" />
{config.elements.themeSwitch && (
<SwitchTheme client:only="react" />
)}
</body>
</html>
123 changes: 123 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import fs from 'fs';
import path from 'path';
import yaml from 'js-yaml';

interface SiteConfig {
sections: {
about: boolean;
workExperience: boolean;
talks: boolean;
writing: boolean;
socialLinks: boolean;
};
elements: {
avatar: boolean;
themeSwitch: boolean;
header: boolean;
footer: boolean;
};
}

const defaultConfig: SiteConfig = {
sections: {
about: true,
workExperience: true,
talks: true,
writing: true,
socialLinks: true,
},
elements: {
avatar: true,
themeSwitch: true,
footer: true,
header: true,
},
};

function deepMerge(target: any, source: any): any {
// Recursively merge source into target
for (const key of Object.keys(source)) {
if (
source[key] &&
typeof source[key] === 'object' &&
!Array.isArray(source[key])
) {
if (!target[key] || typeof target[key] !== 'object') {
target[key] = {};
}
deepMerge(target[key], source[key]);
} else {
if (target[key] === undefined) {
target[key] = source[key];
}
}
}
return target;
}

function isValidConfig(config: any): boolean {
if (
!config ||
typeof config !== 'object' ||
!config.sections ||
typeof config.sections !== 'object' ||
!config.elements ||
typeof config.elements !== 'object'
) {
return false;
}
// Optionally, check for required keys
const sectionKeys = [
'about',
'workExperience',
'talks',
'writing',
'socialLinks',
];
const elementKeys = [
'avatar',
'themeSwitch',
'header',
'footer',
];
for (const key of sectionKeys) {
if (typeof config.sections[key] !== 'boolean') {
return false;
}
}
for (const key of elementKeys) {
if (typeof config.elements[key] !== 'boolean') {
return false;
}
}
return true;
}
Comment on lines +58 to +94
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shrvansudhakara What do you think about for use Zed or Yup for schema validation instead of this approach proposed by Copilot?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderdiaz, There is no production issue, the config works correctly:

  • Dev mode: Reads config.yml on each request, changes reflect immediately
  • Build: Reads config once per component, generates static HTML
  • Production: Static files only, no config reading at runtime

Copilot flagged it as a "performance concern," but it's a non-issue:

  • File reads are ~1-2ms (negligible in builds)
  • Caching would break dev hot-reload
  • Each build is a fresh process anyway

The implementation is correct as-is for Astro SSG. Just addressing the Copilot reviews for completeness.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderdiaz, Using Zod/Yup would be much cleaner and more maintainable, however it would be an add-on on dependency list. I'd prefer Zod, as I worked with it previously.


export function getSiteConfig(): SiteConfig {
const configPath = path.join(process.cwd(), 'config.yml');
let loadedConfig: any = {};
try {
const fileContents = fs.readFileSync(configPath, 'utf8');
loadedConfig = yaml.load(fileContents);
if (!loadedConfig || typeof loadedConfig !== 'object') {
console.warn('config.yml is empty or not a valid YAML object, using defaults');
return defaultConfig;
}
// Merge loaded config with defaults
const mergedConfig = deepMerge(loadedConfig, defaultConfig);
if (!isValidConfig(mergedConfig)) {
console.warn('config.yml is malformed or missing required fields, using defaults');
return defaultConfig;
}
return mergedConfig as SiteConfig;
} catch (error: any) {
if (error.code === 'ENOENT') {
console.warn('config.yml not found, using defaults');
} else if (error.name === 'YAMLException' || error instanceof yaml.YAMLException) {
console.warn('config.yml is malformed YAML, using defaults');
} else {
console.warn(`Error loading config.yml: ${error.message}, using defaults`);
}
return defaultConfig;
}
}
37 changes: 22 additions & 15 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DEFAULT_CONFIGURATION } from '@/lib/constants';
import WorkExperience from '@/components/ui/WorkExperience.astro';
import Talk from '@/components/ui/Talk.astro';
import { sortByDateRange, sortByYear } from '@/lib/utils';
import { getSiteConfig } from '@/lib/config';

const entry = await getEntry('pages', 'homepage');

Expand All @@ -23,25 +24,31 @@ const sortedJobs = sortByDateRange(jobs);

const talks = await getCollection('talks');
const sortedTalks = sortByYear(talks);

const config = getSiteConfig();
---

<BaseLayout seo={entry.data.seo}>
<Container as="section" class="py-6">
<Author {...DEFAULT_CONFIGURATION.author} />
</Container>
{config.elements.avatar && (
<Container as="section" class="py-6">
<Author {...DEFAULT_CONFIGURATION.author} />
</Container>
)}

<Container as="section" class="py-6">
<div class="flex flex-col gap-6">
<div class="flex items-center">
<span class="text-headings">About</span>
</div>
<div class="prose dark:prose-invert">
<Content />
{config.sections.about && (
<Container as="section" class="py-6">
<div class="flex flex-col gap-6">
<div class="flex items-center">
<span class="text-headings">About</span>
</div>
<div class="prose dark:prose-invert">
<Content />
</div>
</div>
</div>
</Container>
</Container>
)}
{
links.length > 0 && (
config.sections.socialLinks && links.length > 0 && (
<Container as="section" class="py-8">
<div class="flex flex-col gap-5">
<span class="text-headings">Contact</span>
Expand Down Expand Up @@ -69,7 +76,7 @@ const sortedTalks = sortByYear(talks);
)
}
{
sortedJobs.length > 0 && (
config.sections.workExperience && sortedJobs.length > 0 && (
<Container as="section" class="py-6">
<div class="flex flex-col gap-5">
<span class="text-headings">Work Experience</span>
Expand All @@ -83,7 +90,7 @@ const sortedTalks = sortByYear(talks);
)
}
{
talks.length > 0 && (
config.sections.talks && sortedTalks.length > 0 && (
<Container as="section" class="py-6">
<div class="flex flex-col gap-5">
<span class="text-headings">Speaking</span>
Expand Down