Updated site completely
This commit is contained in:
@@ -8,12 +8,12 @@ import CompactSteps from '~/components/widgets/CompactSteps.astro';
|
||||
import WorkExperience from '~/components/widgets/WorkExperience.astro';
|
||||
import CompactCertifications from '~/components/widgets/CompactCertifications.astro';
|
||||
import CompactSkills from '~/components/widgets/CompactSkills.astro';
|
||||
import HomePageImage from '~/assets/images/richardbergsma.png'
|
||||
import HomePageImage from '~/assets/images/richardbergsma.png';
|
||||
|
||||
import { getTranslation, supportedLanguages } from '~/i18n/translations';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return supportedLanguages.map(lang => ({
|
||||
return supportedLanguages.map((lang) => ({
|
||||
params: { lang },
|
||||
}));
|
||||
}
|
||||
@@ -26,39 +26,37 @@ if (!supportedLanguages.includes(lang)) {
|
||||
const t = getTranslation(lang);
|
||||
|
||||
const metadata = {
|
||||
title: 'About Me - ' + t.metadata.title,
|
||||
title: `About Me - Richard Bergsma - IT Systems and Automation Manager - ${t.metadata.title}`,
|
||||
description: t.hero.subtitle + ' - IT professional with experience in systems and automation, working at COFRA Holding in Amsterdam.',
|
||||
};
|
||||
---
|
||||
|
||||
<Layout metadata={metadata}>
|
||||
<Fragment slot="announcement"></Fragment>
|
||||
|
||||
|
||||
<!-- Person Structured Data for SEO -->
|
||||
<StructuredData slot="structured-data" data={{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Person",
|
||||
"name": "Richard Bergsma",
|
||||
"jobTitle": "IT Systems and Automation Manager",
|
||||
"description": t.hero.subtitle,
|
||||
"image": Astro.url.origin + "/images/richardbergsma.png",
|
||||
"url": Astro.url.origin,
|
||||
"sameAs": [
|
||||
"https://www.linkedin.com/in/rrpbergsma",
|
||||
"https://github.com/rrpbergsma"
|
||||
],
|
||||
"knowsAbout": t.skills.items.map(skill => skill.title),
|
||||
"worksFor": {
|
||||
"@type": "Organization",
|
||||
"name": "COFRA Holding C.V.",
|
||||
"location": "Amsterdam"
|
||||
}
|
||||
}} />
|
||||
<StructuredData
|
||||
slot="structured-data"
|
||||
data={{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Person',
|
||||
name: 'Richard Bergsma',
|
||||
jobTitle: 'IT Systems and Automation Manager',
|
||||
description: t.hero.subtitle,
|
||||
image: Astro.url.origin + '/images/richardbergsma.png',
|
||||
url: Astro.url.origin,
|
||||
sameAs: ['https://www.linkedin.com/in/rrpbergsma', 'https://github.com/rrpbergsma'],
|
||||
knowsAbout: t.skills.items.map((skill) => skill.title),
|
||||
worksFor: {
|
||||
'@type': 'Organization',
|
||||
name: 'COFRA Holding C.V.',
|
||||
location: 'Amsterdam',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<!-- Hero Widget -->
|
||||
<Hero
|
||||
id="hero"
|
||||
isDark={false}
|
||||
>
|
||||
<Hero id="hero" isDark={false}>
|
||||
<Fragment slot="subtitle">
|
||||
<strong class="text-3xl md:text-4xl">{t.hero.greeting}</strong><br /><br />{t.hero.subtitle}
|
||||
</Fragment>
|
||||
@@ -77,10 +75,14 @@ const metadata = {
|
||||
>
|
||||
<Fragment slot="content">
|
||||
<h2 class="text-3xl font-bold tracking-tight sm:text-4xl mb-2">{t.about.title}</h2>
|
||||
{t.about.content.map((paragraph) => (
|
||||
<p>{paragraph}</p>
|
||||
<br />
|
||||
))}
|
||||
{
|
||||
t.about.content.map((paragraph) => (
|
||||
<>
|
||||
<p>{paragraph}</p>
|
||||
<br />
|
||||
</>
|
||||
))
|
||||
}
|
||||
</Fragment>
|
||||
|
||||
<Fragment slot="bg">
|
||||
@@ -93,7 +95,7 @@ const metadata = {
|
||||
id="resume"
|
||||
title={t.resume.title}
|
||||
compact={true}
|
||||
items={t.resume.experience.map(exp => ({
|
||||
items={t.resume.experience.map((exp) => ({
|
||||
title: exp.title,
|
||||
company: exp.company,
|
||||
date: exp.period,
|
||||
@@ -113,7 +115,7 @@ const metadata = {
|
||||
issueDate: cert.issueDate,
|
||||
description: cert.description,
|
||||
linkUrl: cert.linkUrl,
|
||||
image: cert.image
|
||||
image: cert.image,
|
||||
}))}
|
||||
/>
|
||||
|
||||
@@ -123,7 +125,7 @@ const metadata = {
|
||||
title={t.skills.title}
|
||||
subtitle={t.skills.subtitle}
|
||||
defaultIcon="tabler:point-filled"
|
||||
items={t.skills.items.map(item => ({
|
||||
items={t.skills.items.map((item) => ({
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
}))}
|
||||
@@ -133,9 +135,9 @@ const metadata = {
|
||||
<CompactSteps
|
||||
id="education"
|
||||
title={t.education.title}
|
||||
items={t.education.items.map(item => ({
|
||||
items={t.education.items.map((item) => ({
|
||||
title: item.title,
|
||||
icon: 'tabler:school'
|
||||
icon: 'tabler:school',
|
||||
}))}
|
||||
/>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getTranslation, supportedLanguages } from '~/i18n/translations';
|
||||
import OurCommitmentImage from '~/assets/images/OurCommitment.webp';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return supportedLanguages.map(lang => ({
|
||||
return supportedLanguages.map((lang) => ({
|
||||
params: { lang },
|
||||
}));
|
||||
}
|
||||
@@ -52,72 +52,75 @@ const metadata = {
|
||||
<!-- Features Widget -->
|
||||
<Features
|
||||
id="services"
|
||||
tagline={t.homepage?.services?.tagline || "Services"}
|
||||
title={t.homepage?.services?.title || "How I Can Help Your Organization"}
|
||||
subtitle={t.homepage?.services?.subtitle || "I offer a range of specialized IT services to help businesses optimize their operations and digital infrastructure."}
|
||||
items={(t.homepage?.services?.items || [
|
||||
{
|
||||
title: 'Workflow Automation',
|
||||
description:
|
||||
'Streamline your business processes with Power Automate solutions that reduce manual effort and increase operational efficiency.',
|
||||
icon: 'tabler:settings-automation',
|
||||
},
|
||||
{
|
||||
title: 'Intelligent Chatbots',
|
||||
description:
|
||||
'Develop smart chatbots in Copilot Studio that enhance user interactions through natural language processing and automated responses.',
|
||||
icon: 'tabler:message-chatbot',
|
||||
},
|
||||
{
|
||||
title: 'API Integrations',
|
||||
description:
|
||||
'Create seamless connections between your applications and services with custom API integrations for efficient data exchange.',
|
||||
icon: 'tabler:api',
|
||||
},
|
||||
{
|
||||
title: 'Microsoft 365 Management',
|
||||
description:
|
||||
'Optimize your Microsoft 365 environment with expert administration, security configurations, and service optimization.',
|
||||
icon: 'tabler:brand-office',
|
||||
},
|
||||
{
|
||||
title: 'SharePoint Solutions',
|
||||
description:
|
||||
'Set up, manage, and optimize SharePoint Online and on-premise deployments for effective document management and collaboration.',
|
||||
icon: 'tabler:share',
|
||||
},
|
||||
{
|
||||
title: 'IT Infrastructure Oversight',
|
||||
description:
|
||||
'Manage global IT infrastructures, including servers, networks, and end-user devices to ensure reliable operations.',
|
||||
icon: 'tabler:server',
|
||||
},
|
||||
]).map(item => ({...item, icon: item.icon || 'tabler:check'}))}
|
||||
tagline={t.homepage?.services?.tagline || 'Services'}
|
||||
title={t.homepage?.services?.title || 'How I Can Help Your Organization'}
|
||||
subtitle={t.homepage?.services?.subtitle ||
|
||||
'I offer a range of specialized IT services to help businesses optimize their operations and digital infrastructure.'}
|
||||
items={(
|
||||
t.homepage?.services?.items || [
|
||||
{
|
||||
title: 'Workflow Automation',
|
||||
description:
|
||||
'Streamline your business processes with Power Automate solutions that reduce manual effort and increase operational efficiency.',
|
||||
icon: 'tabler:settings-automation',
|
||||
},
|
||||
{
|
||||
title: 'Intelligent Chatbots',
|
||||
description:
|
||||
'Develop smart chatbots in Copilot Studio that enhance user interactions through natural language processing and automated responses.',
|
||||
icon: 'tabler:message-chatbot',
|
||||
},
|
||||
{
|
||||
title: 'API Integrations',
|
||||
description:
|
||||
'Create seamless connections between your applications and services with custom API integrations for efficient data exchange.',
|
||||
icon: 'tabler:api',
|
||||
},
|
||||
{
|
||||
title: 'Microsoft 365 Management',
|
||||
description:
|
||||
'Optimize your Microsoft 365 environment with expert administration, security configurations, and service optimization.',
|
||||
icon: 'tabler:brand-office',
|
||||
},
|
||||
{
|
||||
title: 'SharePoint Solutions',
|
||||
description:
|
||||
'Set up, manage, and optimize SharePoint Online and on-premise deployments for effective document management and collaboration.',
|
||||
icon: 'tabler:share',
|
||||
},
|
||||
{
|
||||
title: 'IT Infrastructure Oversight',
|
||||
description:
|
||||
'Manage global IT infrastructures, including servers, networks, and end-user devices to ensure reliable operations.',
|
||||
icon: 'tabler:server',
|
||||
},
|
||||
]
|
||||
).map((item) => ({ ...item, icon: item.icon || 'tabler:check' }))}
|
||||
/>
|
||||
|
||||
<!-- Content Widget -->
|
||||
<Content
|
||||
isReversed
|
||||
tagline={t.homepage?.approach?.tagline || "About My Approach"}
|
||||
title={t.homepage?.approach?.title || "Our Commitment"}
|
||||
tagline={t.homepage?.approach?.tagline || 'About My Approach'}
|
||||
title={t.homepage?.approach?.title || 'Our Commitment'}
|
||||
items={[]}
|
||||
image={{
|
||||
src: OurCommitmentImage,
|
||||
alt: 'IT Excellence and Innovation'
|
||||
alt: 'IT Excellence and Innovation',
|
||||
}}
|
||||
>
|
||||
<Fragment slot="content">
|
||||
<div class="text-lg dark:text-slate-400">
|
||||
{(t.homepage?.approach?.missionContent || [
|
||||
'We are committed to driving IT excellence through strategic cloud optimization, process automation, and enterprise-grade technical support. We leverage cutting-edge technology to address complex business challenges and deliver measurable value.',
|
||||
'With deep expertise in Microsoft technologies and automation, we empower organizations to transform their digital capabilities and achieve their business objectives. We design solutions that enhance user experience and maximize productivity, ensuring technology empowers your business.',
|
||||
'We stay ahead of the curve by researching and implementing emerging technologies, providing scalable solutions that adapt to your evolving needs. Our approach aligns technical solutions with your core business objectives, delivering measurable ROI and a competitive advantage.',
|
||||
'Our mission is to leverage technology to solve real business challenges and create value through innovation. With over 15 years of IT experience, we bring a wealth of knowledge in Microsoft technologies, automation tools, and system integration to help organizations transform their digital capabilities and achieve their strategic goals.'
|
||||
]).map((paragraph, index, array) => (
|
||||
<p class={index === array.length - 1 ? '' : 'mb-4'}>
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
{
|
||||
(
|
||||
t.homepage?.approach?.missionContent || [
|
||||
'We are committed to driving IT excellence through strategic cloud optimization, process automation, and enterprise-grade technical support. We leverage cutting-edge technology to address complex business challenges and deliver measurable value.',
|
||||
'With deep expertise in Microsoft technologies and automation, we empower organizations to transform their digital capabilities and achieve their business objectives. We design solutions that enhance user experience and maximize productivity, ensuring technology empowers your business.',
|
||||
'We stay ahead of the curve by researching and implementing emerging technologies, providing scalable solutions that adapt to your evolving needs. Our approach aligns technical solutions with your core business objectives, delivering measurable ROI and a competitive advantage.',
|
||||
'Our mission is to leverage technology to solve real business challenges and create value through innovation. With over 15 years of IT experience, we bring a wealth of knowledge in Microsoft technologies, automation tools, and system integration to help organizations transform their digital capabilities and achieve their strategic goals.',
|
||||
]
|
||||
).map((paragraph, index, array) => <p class={index === array.length - 1 ? '' : 'mb-4'}>{paragraph}</p>)
|
||||
}
|
||||
</div>
|
||||
</Fragment>
|
||||
</Content>
|
||||
@@ -164,15 +167,19 @@ const metadata = {
|
||||
>
|
||||
<Fragment slot="title">{t.homepage?.callToAction?.title || 'Ready to optimize your IT systems?'}</Fragment>
|
||||
<Fragment slot="subtitle">
|
||||
{t.homepage?.callToAction?.subtitle || 'Let\'s discuss how I can help your organization streamline processes, enhance collaboration, and drive digital transformation.'}
|
||||
{
|
||||
t.homepage?.callToAction?.subtitle ||
|
||||
"Let's discuss how I can help your organization streamline processes, enhance collaboration, and drive digital transformation."
|
||||
}
|
||||
</Fragment>
|
||||
</CallToAction>
|
||||
|
||||
<!-- Contact Widget -->
|
||||
<Contact
|
||||
id="contact"
|
||||
title={t.homepage?.contact?.title || "Get in Touch"}
|
||||
subtitle={t.homepage?.contact?.subtitle || "Have a project in mind or questions about my services? Reach out and let's start a conversation."}
|
||||
title={t.homepage?.contact?.title || 'Get in Touch'}
|
||||
subtitle={t.homepage?.contact?.subtitle ||
|
||||
"Have a project in mind or questions about my services? Reach out and let's start a conversation."}
|
||||
inputs={[
|
||||
{
|
||||
type: 'text',
|
||||
@@ -193,11 +200,11 @@ const metadata = {
|
||||
rows: 8,
|
||||
}}
|
||||
disclaimer={{
|
||||
label: t.homepage?.contact?.disclaimer ||
|
||||
label:
|
||||
t.homepage?.contact?.disclaimer ||
|
||||
'By submitting this form, you agree to our privacy policy and allow us to use your information to contact you about our services.',
|
||||
}}
|
||||
description={t.homepage?.contact?.description || "I'll respond to your message as soon as possible. You can also connect with me on LinkedIn or GitHub."}
|
||||
description={t.homepage?.contact?.description ||
|
||||
"I'll respond to your message as soon as possible. You can also connect with me on LinkedIn or GitHub."}
|
||||
/>
|
||||
|
||||
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
@@ -7,7 +7,7 @@ import Hero from '~/components/widgets/Hero.astro';
|
||||
import { getTranslation, supportedLanguages } from '~/i18n/translations';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return supportedLanguages.map(lang => ({
|
||||
return supportedLanguages.map((lang) => ({
|
||||
params: { lang },
|
||||
}));
|
||||
}
|
||||
@@ -41,31 +41,28 @@ const tocItems = [
|
||||
|
||||
<Layout metadata={metadata}>
|
||||
<Fragment slot="announcement"></Fragment>
|
||||
|
||||
|
||||
<!-- Legal Document Structured Data for SEO -->
|
||||
<StructuredData slot="structured-data" data={{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"name": "Privacy Policy",
|
||||
"description": "Privacy Policy outlining our data collection practices, cookie usage, and your rights under GDPR.",
|
||||
"url": Astro.url.origin + "/" + lang + "/privacy",
|
||||
"mainEntity": {
|
||||
"@type": "Article",
|
||||
"headline": "Privacy Policy",
|
||||
"datePublished": "2025-03-06",
|
||||
"dateModified": "2025-03-06"
|
||||
}
|
||||
}} />
|
||||
<StructuredData
|
||||
slot="structured-data"
|
||||
data={{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
name: 'Privacy Policy',
|
||||
description: 'Privacy Policy outlining our data collection practices, cookie usage, and your rights under GDPR.',
|
||||
url: Astro.url.origin + '/' + lang + '/privacy',
|
||||
mainEntity: {
|
||||
'@type': 'Article',
|
||||
headline: 'Privacy Policy',
|
||||
datePublished: '2025-03-06',
|
||||
dateModified: '2025-03-06',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<!-- Hero Widget -->
|
||||
<Hero
|
||||
id="hero"
|
||||
title={t.footer.privacyPolicy}
|
||||
isDark={false}
|
||||
>
|
||||
<Fragment slot="subtitle">
|
||||
Last updated: March 6, 2025 (Added cookie consent banner)
|
||||
</Fragment>
|
||||
<Hero id="hero" title={t.footer.privacyPolicy} isDark={false}>
|
||||
<Fragment slot="subtitle"> Last updated: March 6, 2025 (Added cookie consent banner) </Fragment>
|
||||
</Hero>
|
||||
|
||||
<!-- Content Widget -->
|
||||
@@ -74,37 +71,48 @@ const tocItems = [
|
||||
<div class="bg-gray-50 dark:bg-slate-800 p-5 rounded-lg mb-10">
|
||||
<h2 class="text-xl font-bold mb-3">Table of Contents</h2>
|
||||
<ul class="space-y-2">
|
||||
{tocItems.map(item => (
|
||||
<li>
|
||||
<a href={`#${item.id}`} class="text-blue-600 dark:text-blue-400 hover:underline">
|
||||
{item.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
{
|
||||
tocItems.map((item) => (
|
||||
<li>
|
||||
<a href={`#${item.id}`} class="text-blue-600 dark:text-blue-400 hover:underline">
|
||||
{item.title}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Privacy Policy Content -->
|
||||
<div class="prose prose-lg max-w-4xl dark:prose-invert dark:prose-headings:text-slate-300 prose-md prose-headings:font-heading prose-headings:leading-tighter prose-headings:tracking-tighter prose-headings:font-bold prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-img:rounded-md prose-img:shadow-lg backdrop-blur-sm bg-white/15 dark:bg-slate-900/30 p-6 rounded-lg border border-gray-200 dark:border-slate-800">
|
||||
<div
|
||||
class="prose prose-lg max-w-4xl dark:prose-invert dark:prose-headings:text-slate-300 prose-md prose-headings:font-heading prose-headings:leading-tighter prose-headings:tracking-tighter prose-headings:font-bold prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-img:rounded-md prose-img:shadow-lg backdrop-blur-sm bg-white/15 dark:bg-slate-900/30 p-6 rounded-lg border border-gray-200 dark:border-slate-800"
|
||||
>
|
||||
<h2 id="introduction" class="text-2xl font-bold mt-8 mb-4">1. Introduction</h2>
|
||||
<p>
|
||||
This Privacy Policy explains how we handle information when you visit our website. We are committed to protecting your privacy and complying with applicable data protection laws, including the General Data Protection Regulation (GDPR).
|
||||
This Privacy Policy explains how we handle information when you visit our website. We are committed to
|
||||
protecting your privacy and complying with applicable data protection laws, including the General Data
|
||||
Protection Regulation (GDPR).
|
||||
</p>
|
||||
<p>
|
||||
We value transparency and want you to understand what information we collect, why we collect it, and how we use it. This policy applies to all visitors to our website.
|
||||
We value transparency and want you to understand what information we collect, why we collect it, and how we use
|
||||
it. This policy applies to all visitors to our website.
|
||||
</p>
|
||||
<h2 id="data-collection" class="text-2xl font-bold mt-8 mb-4">2. Data Collection Policy</h2>
|
||||
<p>
|
||||
<strong>We do not collect or store any personal user data.</strong> Our website is designed to provide information without requiring you to submit any personal information.
|
||||
<strong>We do not collect or store any personal user data.</strong> Our website is designed to provide information
|
||||
without requiring you to submit any personal information.
|
||||
</p>
|
||||
<p>
|
||||
The only data stored is your preferences for language and theme settings, which are stored locally on your device using browser technologies (cookies and LocalStorage) and are never transmitted to our servers. More details about this are provided in the sections below.
|
||||
</p>
|
||||
<p>
|
||||
We do not:
|
||||
The only data stored is your preferences for language and theme settings, which are stored locally on your
|
||||
device using browser technologies (cookies and LocalStorage) and are never transmitted to our servers. More
|
||||
details about this are provided in the sections below.
|
||||
</p>
|
||||
<p>We do not:</p>
|
||||
<ul>
|
||||
<li>Collect your name, email address, or other contact information unless you voluntarily provide it through our contact form</li>
|
||||
<li>
|
||||
Collect your name, email address, or other contact information unless you voluntarily provide it through our
|
||||
contact form
|
||||
</li>
|
||||
<li>Track your browsing behavior</li>
|
||||
<li>Use analytics services that collect personal data</li>
|
||||
<li>Use advertising or marketing tracking technologies</li>
|
||||
@@ -112,15 +120,16 @@ const tocItems = [
|
||||
<li>Store your preferences on our servers</li>
|
||||
</ul>
|
||||
<p>
|
||||
If you choose to contact us using our contact form, the information you provide (such as your name and email address) will only be used to respond to your inquiry and will not be stored longer than necessary for that purpose.
|
||||
If you choose to contact us using our contact form, the information you provide (such as your name and email
|
||||
address) will only be used to respond to your inquiry and will not be stored longer than necessary for that
|
||||
purpose.
|
||||
</p>
|
||||
<h2 id="cookie-usage" class="text-2xl font-bold mt-8 mb-4">3. Cookie & Storage Usage</h2>
|
||||
<p>
|
||||
<strong>Our website uses cookies strictly for essential functionality.</strong> These cookies are necessary for the proper functioning of our website and do not collect any personal information.
|
||||
</p>
|
||||
<p>
|
||||
Details about the cookies we use:
|
||||
<strong>Our website uses cookies strictly for essential functionality.</strong> These cookies are necessary for the
|
||||
proper functioning of our website and do not collect any personal information.
|
||||
</p>
|
||||
<p>Details about the cookies we use:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Name:</strong> preferredLanguage
|
||||
@@ -142,20 +151,23 @@ const tocItems = [
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
In addition to cookies, we also use LocalStorage to store certain preferences. Details about this are provided in the next section.
|
||||
In addition to cookies, we also use LocalStorage to store certain preferences. Details about this are provided
|
||||
in the next section.
|
||||
</p>
|
||||
<p>
|
||||
We do not use any tracking, analytics, or third-party cookies. No personal information is collected through our cookies or LocalStorage.
|
||||
We do not use any tracking, analytics, or third-party cookies. No personal information is collected through our
|
||||
cookies or LocalStorage.
|
||||
</p>
|
||||
<h2 id="localstorage" class="text-2xl font-bold mt-8 mb-4">4. LocalStorage Usage</h2>
|
||||
<p>
|
||||
<strong>Our website uses LocalStorage to enhance your experience by remembering your preferences and consent choices.</strong>
|
||||
</p>
|
||||
<p>
|
||||
Details about our LocalStorage usage:
|
||||
<strong
|
||||
>Our website uses LocalStorage to enhance your experience by remembering your preferences and consent choices.</strong
|
||||
>
|
||||
</p>
|
||||
<p>Details about our LocalStorage usage:</p>
|
||||
<ul>
|
||||
<li><strong>Data stored:</strong>
|
||||
<li>
|
||||
<strong>Data stored:</strong>
|
||||
<ul>
|
||||
<li>Theme preference (light/dark mode)</li>
|
||||
<li>Cookie consent acceptance status</li>
|
||||
@@ -166,14 +178,18 @@ const tocItems = [
|
||||
<li><strong>Duration:</strong> Persists until you clear your browser's LocalStorage</li>
|
||||
</ul>
|
||||
<p>
|
||||
LocalStorage is a technology that allows websites to store data directly in your browser. Unlike cookies, LocalStorage data is not sent with every request to the server, which makes it more efficient for storing user preferences that only need to be accessed by your browser.
|
||||
LocalStorage is a technology that allows websites to store data directly in your browser. Unlike cookies,
|
||||
LocalStorage data is not sent with every request to the server, which makes it more efficient for storing user
|
||||
preferences that only need to be accessed by your browser.
|
||||
</p>
|
||||
<p>
|
||||
No personal information is collected or stored in LocalStorage. The data is used solely to enhance your browsing experience by maintaining your preferred settings.
|
||||
No personal information is collected or stored in LocalStorage. The data is used solely to enhance your browsing
|
||||
experience by maintaining your preferred settings.
|
||||
</p>
|
||||
<h2 id="clear-preferences" class="text-2xl font-bold mt-8 mb-4">5. How to Clear Your Preferences</h2>
|
||||
<p>
|
||||
If you wish to reset your language or theme settings, you can clear your browser's cookies and LocalStorage data. Here's how to do it in common browsers:
|
||||
If you wish to reset your language or theme settings, you can clear your browser's cookies and LocalStorage
|
||||
data. Here's how to do it in common browsers:
|
||||
</p>
|
||||
<p>
|
||||
<strong>Chrome:</strong>
|
||||
@@ -208,44 +224,59 @@ const tocItems = [
|
||||
<li>Find our website and click "Remove" or "Remove All"</li>
|
||||
</ol>
|
||||
<p>
|
||||
After clearing your browser data, your language will reset to the default (English) and your theme will reset to the system default.
|
||||
After clearing your browser data, your language will reset to the default (English) and your theme will reset to
|
||||
the system default.
|
||||
</p>
|
||||
<h2 id="user-rights" class="text-2xl font-bold mt-8 mb-4">6. Your Rights (GDPR Compliance)</h2>
|
||||
<p>
|
||||
Under the General Data Protection Regulation (GDPR), you have various rights regarding your personal data. However, since we do not collect or store personal data (except for the language preference cookie which does not contain personal information), most of these rights are not applicable in practice.
|
||||
</p>
|
||||
<p>
|
||||
Nevertheless, you have the right to:
|
||||
Under the General Data Protection Regulation (GDPR), you have various rights regarding your personal data.
|
||||
However, since we do not collect or store personal data (except for the language preference cookie which does
|
||||
not contain personal information), most of these rights are not applicable in practice.
|
||||
</p>
|
||||
<p>Nevertheless, you have the right to:</p>
|
||||
<ul>
|
||||
<li><strong>Delete your cookie and LocalStorage data:</strong> You can delete the language preference cookie and theme preference LocalStorage data at any time through your browser settings (see section 5 for instructions)</li>
|
||||
<li><strong>Be informed:</strong> This privacy policy provides transparent information about our data practices</li>
|
||||
<li>
|
||||
<strong>Delete your cookie and LocalStorage data:</strong> You can delete the language preference cookie and theme
|
||||
preference LocalStorage data at any time through your browser settings (see section 5 for instructions)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Be informed:</strong> This privacy policy provides transparent information about our data practices
|
||||
</li>
|
||||
<li><strong>Object:</strong> You can choose to disable cookies and LocalStorage in your browser settings</li>
|
||||
</ul>
|
||||
<p>
|
||||
If you have any questions about your rights or wish to exercise any of them, please contact us using the information provided at the end of this policy.
|
||||
If you have any questions about your rights or wish to exercise any of them, please contact us using the
|
||||
information provided at the end of this policy.
|
||||
</p>
|
||||
<h2 id="data-security" class="text-2xl font-bold mt-8 mb-4">7. Data Security</h2>
|
||||
<p>
|
||||
We take appropriate technical and organizational measures to ensure the security of any information transmitted to us. However, please be aware that no method of transmission over the internet or method of electronic storage is 100% secure.
|
||||
We take appropriate technical and organizational measures to ensure the security of any information transmitted
|
||||
to us. However, please be aware that no method of transmission over the internet or method of electronic storage
|
||||
is 100% secure.
|
||||
</p>
|
||||
<p>
|
||||
Our website uses HTTPS encryption to ensure that any communication between your browser and our website is secure.
|
||||
Our website uses HTTPS encryption to ensure that any communication between your browser and our website is
|
||||
secure.
|
||||
</p>
|
||||
<h2 id="third-party" class="text-2xl font-bold mt-8 mb-4">8. Third-Party Websites</h2>
|
||||
<p>
|
||||
Our website may contain links to other websites that are not operated by us. If you click on a third-party link, you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every site you visit.
|
||||
Our website may contain links to other websites that are not operated by us. If you click on a third-party link,
|
||||
you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every
|
||||
site you visit.
|
||||
</p>
|
||||
<p>
|
||||
We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.
|
||||
We have no control over and assume no responsibility for the content, privacy policies, or practices of any
|
||||
third-party sites or services.
|
||||
</p>
|
||||
<h2 id="changes" class="text-2xl font-bold mt-8 mb-4">9. Changes to Privacy Policy</h2>
|
||||
<p>
|
||||
We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the "Last updated" date at the top of this page.
|
||||
We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy
|
||||
Policy on this page and updating the "Last updated" date at the top of this page.
|
||||
</p>
|
||||
<p>
|
||||
You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.
|
||||
</p>
|
||||
You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are
|
||||
effective when they are posted on this page.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
@@ -7,7 +7,7 @@ import Hero from '~/components/widgets/Hero.astro';
|
||||
import { getTranslation, supportedLanguages } from '~/i18n/translations';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return supportedLanguages.map(lang => ({
|
||||
return supportedLanguages.map((lang) => ({
|
||||
params: { lang },
|
||||
}));
|
||||
}
|
||||
@@ -39,31 +39,29 @@ const tocItems = [
|
||||
|
||||
<Layout metadata={metadata}>
|
||||
<Fragment slot="announcement"></Fragment>
|
||||
|
||||
|
||||
<!-- Legal Document Structured Data for SEO -->
|
||||
<StructuredData slot="structured-data" data={{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"name": "Terms and Conditions",
|
||||
"description": "Terms and Conditions for our website, outlining user rights, responsibilities, and legal information.",
|
||||
"url": Astro.url.origin + "/" + lang + "/terms",
|
||||
"mainEntity": {
|
||||
"@type": "Article",
|
||||
"headline": "Terms and Conditions",
|
||||
"datePublished": "2025-03-06",
|
||||
"dateModified": "2025-03-06"
|
||||
}
|
||||
}} />
|
||||
<StructuredData
|
||||
slot="structured-data"
|
||||
data={{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
name: 'Terms and Conditions',
|
||||
description:
|
||||
'Terms and Conditions for our website, outlining user rights, responsibilities, and legal information.',
|
||||
url: Astro.url.origin + '/' + lang + '/terms',
|
||||
mainEntity: {
|
||||
'@type': 'Article',
|
||||
headline: 'Terms and Conditions',
|
||||
datePublished: '2025-03-06',
|
||||
dateModified: '2025-03-06',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<!-- Hero Widget -->
|
||||
<Hero
|
||||
id="hero"
|
||||
title={t.footer.terms}
|
||||
isDark={false}
|
||||
>
|
||||
<Fragment slot="subtitle">
|
||||
Last updated: March 6, 2025
|
||||
</Fragment>
|
||||
<Hero id="hero" title={t.footer.terms} isDark={false}>
|
||||
<Fragment slot="subtitle"> Last updated: March 6, 2025 </Fragment>
|
||||
</Hero>
|
||||
|
||||
<!-- Content Widget -->
|
||||
@@ -72,75 +70,103 @@ const tocItems = [
|
||||
<div class="bg-gray-50 dark:bg-slate-800 p-5 rounded-lg mb-10">
|
||||
<h2 class="text-xl font-bold mb-3">Table of Contents</h2>
|
||||
<ul class="space-y-2">
|
||||
{tocItems.map(item => (
|
||||
<li>
|
||||
<a href={`#${item.id}`} class="text-blue-600 dark:text-blue-400 hover:underline">
|
||||
{item.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
{
|
||||
tocItems.map((item) => (
|
||||
<li>
|
||||
<a href={`#${item.id}`} class="text-blue-600 dark:text-blue-400 hover:underline">
|
||||
{item.title}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Terms Content -->
|
||||
<div class="prose prose-lg max-w-4xl dark:prose-invert dark:prose-headings:text-slate-300 prose-md prose-headings:font-heading prose-headings:leading-tighter prose-headings:tracking-tighter prose-headings:font-bold prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-img:rounded-md prose-img:shadow-lg backdrop-blur-sm bg-white/15 dark:bg-slate-900/30 p-6 rounded-lg border border-gray-200 dark:border-slate-800">
|
||||
<div
|
||||
class="prose prose-lg max-w-4xl dark:prose-invert dark:prose-headings:text-slate-300 prose-md prose-headings:font-heading prose-headings:leading-tighter prose-headings:tracking-tighter prose-headings:font-bold prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-img:rounded-md prose-img:shadow-lg backdrop-blur-sm bg-white/15 dark:bg-slate-900/30 p-6 rounded-lg border border-gray-200 dark:border-slate-800"
|
||||
>
|
||||
<p>
|
||||
Please read these terms and conditions carefully before using our website. By accessing or using our website, you agree to be bound by these terms and conditions.
|
||||
Please read these terms and conditions carefully before using our website. By accessing or using our website,
|
||||
you agree to be bound by these terms and conditions.
|
||||
</p>
|
||||
|
||||
<h2 id="scope" class="text-2xl font-bold mt-8 mb-4">1. Scope of Services</h2>
|
||||
<p>
|
||||
Our website provides information about our professional services, expertise, and industry insights. The content on this website is for general informational purposes only and does not constitute professional advice. We may update, modify, or remove content at any time without notice.
|
||||
Our website provides information about our professional services, expertise, and industry insights. The content
|
||||
on this website is for general informational purposes only and does not constitute professional advice. We may
|
||||
update, modify, or remove content at any time without notice.
|
||||
</p>
|
||||
|
||||
<h2 id="user-rights" class="text-2xl font-bold mt-8 mb-4">2. User Rights & Responsibilities</h2>
|
||||
<p>
|
||||
When using our website, you agree to:
|
||||
</p>
|
||||
<p>When using our website, you agree to:</p>
|
||||
<ul>
|
||||
<li>Use the website in accordance with these terms and conditions and all applicable laws and regulations</li>
|
||||
<li>Not use the website in any way that could damage, disable, overburden, or impair our services</li>
|
||||
<li>Not attempt to gain unauthorized access to any part of the website or any system or network connected to the website</li>
|
||||
<li>
|
||||
Not attempt to gain unauthorized access to any part of the website or any system or network connected to the
|
||||
website
|
||||
</li>
|
||||
<li>Not use any automated means to access or collect data from the website</li>
|
||||
<li>Not use the website to transmit any harmful code or material</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="intellectual-property" class="text-2xl font-bold mt-8 mb-4">3. Intellectual Property</h2>
|
||||
<p>
|
||||
All content on this website, including but not limited to text, graphics, logos, images, audio clips, digital downloads, and data compilations, is the property of the website owner or its content suppliers and is protected by Dutch and international copyright laws.
|
||||
All content on this website, including but not limited to text, graphics, logos, images, audio clips, digital
|
||||
downloads, and data compilations, is the property of the website owner or its content suppliers and is protected
|
||||
by Dutch and international copyright laws.
|
||||
</p>
|
||||
<p>
|
||||
You may view, download, and print content from this website for your personal, non-commercial use, provided that you do not modify the content and that you retain all copyright and other proprietary notices.
|
||||
You may view, download, and print content from this website for your personal, non-commercial use, provided that
|
||||
you do not modify the content and that you retain all copyright and other proprietary notices.
|
||||
</p>
|
||||
|
||||
<h2 id="liability" class="text-2xl font-bold mt-8 mb-4">4. Limitation of Liability</h2>
|
||||
<p>
|
||||
To the fullest extent permitted by applicable law, we exclude all representations, warranties, and conditions relating to our website and the use of this website. We will not be liable for any direct, indirect, or consequential loss or damage arising under these terms and conditions or in connection with our website, whether arising in tort, contract, or otherwise, including, without limitation, any loss of profit, contracts, business, goodwill, data, income, revenue, or anticipated savings.
|
||||
To the fullest extent permitted by applicable law, we exclude all representations, warranties, and conditions
|
||||
relating to our website and the use of this website. We will not be liable for any direct, indirect, or
|
||||
consequential loss or damage arising under these terms and conditions or in connection with our website, whether
|
||||
arising in tort, contract, or otherwise, including, without limitation, any loss of profit, contracts, business,
|
||||
goodwill, data, income, revenue, or anticipated savings.
|
||||
</p>
|
||||
<p>
|
||||
This does not exclude or limit our liability for death or personal injury resulting from our negligence, nor our liability for fraudulent misrepresentation or misrepresentation as to a fundamental matter, nor any other liability which cannot be excluded or limited under applicable law.
|
||||
This does not exclude or limit our liability for death or personal injury resulting from our negligence, nor our
|
||||
liability for fraudulent misrepresentation or misrepresentation as to a fundamental matter, nor any other
|
||||
liability which cannot be excluded or limited under applicable law.
|
||||
</p>
|
||||
|
||||
<h2 id="governing-law" class="text-2xl font-bold mt-8 mb-4">5. Governing Law</h2>
|
||||
<p>
|
||||
These terms and conditions are governed by and construed in accordance with the laws of the Netherlands. Any disputes relating to these terms and conditions shall be subject to the exclusive jurisdiction of the courts of the Netherlands.
|
||||
These terms and conditions are governed by and construed in accordance with the laws of the Netherlands. Any
|
||||
disputes relating to these terms and conditions shall be subject to the exclusive jurisdiction of the courts of
|
||||
the Netherlands.
|
||||
</p>
|
||||
<p>
|
||||
If you are a consumer, you will benefit from any mandatory provisions of the law of the country in which you are resident. Nothing in these terms and conditions affects your rights as a consumer to rely on such mandatory provisions of local law.
|
||||
If you are a consumer, you will benefit from any mandatory provisions of the law of the country in which you are
|
||||
resident. Nothing in these terms and conditions affects your rights as a consumer to rely on such mandatory
|
||||
provisions of local law.
|
||||
</p>
|
||||
|
||||
<h2 id="cookies" class="text-2xl font-bold mt-8 mb-4">6. Cookie Usage</h2>
|
||||
<p>
|
||||
Our website uses only one cookie, which is used exclusively for storing your selected language preference. This cookie is essential for the proper functioning of the language selection feature on our website. We do not use any tracking, analytics, or third-party cookies.
|
||||
Our website uses only one cookie, which is used exclusively for storing your selected language preference. This
|
||||
cookie is essential for the proper functioning of the language selection feature on our website. We do not use
|
||||
any tracking, analytics, or third-party cookies.
|
||||
</p>
|
||||
<p>
|
||||
The language preference cookie stores only your selected language choice and does not collect any personal information. This cookie is stored on your device for a period of 365 days, after which it will expire unless you visit our website again.
|
||||
The language preference cookie stores only your selected language choice and does not collect any personal
|
||||
information. This cookie is stored on your device for a period of 365 days, after which it will expire unless
|
||||
you visit our website again.
|
||||
</p>
|
||||
|
||||
<h2 id="changes" class="text-2xl font-bold mt-8 mb-4">7. Changes to Terms</h2>
|
||||
<p>
|
||||
We may revise these terms and conditions at any time by amending this page. You are expected to check this page from time to time to take notice of any changes we make, as they are legally binding on you. Some of the provisions contained in these terms and conditions may also be superseded by provisions or notices published elsewhere on our website.
|
||||
We may revise these terms and conditions at any time by amending this page. You are expected to check this page
|
||||
from time to time to take notice of any changes we make, as they are legally binding on you. Some of the
|
||||
provisions contained in these terms and conditions may also be superseded by provisions or notices published
|
||||
elsewhere on our website.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
@@ -4,24 +4,25 @@ import { supportedLanguages } from '~/i18n/translations';
|
||||
|
||||
// Check for language preference in cookies (set by client-side JS)
|
||||
const cookies = Astro.request.headers.get('cookie') || '';
|
||||
const cookieLanguage = cookies.split(';')
|
||||
.map(cookie => cookie.trim())
|
||||
.find(cookie => cookie.startsWith('preferredLanguage='))
|
||||
const cookieLanguage = cookies
|
||||
.split(';')
|
||||
.map((cookie) => cookie.trim())
|
||||
.find((cookie) => cookie.startsWith('preferredLanguage='))
|
||||
?.split('=')[1];
|
||||
|
||||
// Get the user's preferred language from the browser if no cookie
|
||||
const acceptLanguage = Astro.request.headers.get('accept-language') || '';
|
||||
// Define the type for supported languages
|
||||
type SupportedLanguage = typeof supportedLanguages[number];
|
||||
type SupportedLanguage = (typeof supportedLanguages)[number];
|
||||
|
||||
// Use cookie language if available, otherwise detect from browser
|
||||
const preferredLanguage =
|
||||
(cookieLanguage && supportedLanguages.includes(cookieLanguage as SupportedLanguage))
|
||||
cookieLanguage && supportedLanguages.includes(cookieLanguage as SupportedLanguage)
|
||||
? cookieLanguage
|
||||
: acceptLanguage
|
||||
.split(',')
|
||||
.map(lang => lang.split(';')[0].trim().substring(0, 2))
|
||||
.find(lang => supportedLanguages.includes(lang as SupportedLanguage)) || 'en';
|
||||
.map((lang) => lang.split(';')[0].trim().substring(0, 2))
|
||||
.find((lang) => supportedLanguages.includes(lang as SupportedLanguage)) || 'en';
|
||||
|
||||
// Get the hash fragment if present
|
||||
const url = new URL(Astro.request.url);
|
||||
@@ -29,4 +30,4 @@ const hash = url.hash;
|
||||
|
||||
// Redirect to the language-specific about me page
|
||||
return Astro.redirect(`/${preferredLanguage}/aboutme${hash}`);
|
||||
---
|
||||
---
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
validateCsrfToken,
|
||||
checkRateLimit,
|
||||
sendAdminNotification,
|
||||
sendUserConfirmation
|
||||
sendUserConfirmation,
|
||||
} from '../../utils/email-handler';
|
||||
|
||||
// Enhanced email validation with more comprehensive regex
|
||||
@@ -20,52 +20,67 @@ const isSpam = (content: string, name: string, email: string): boolean => {
|
||||
const lowerContent = content.toLowerCase();
|
||||
const lowerName = name.toLowerCase();
|
||||
const lowerEmail = email.toLowerCase();
|
||||
|
||||
|
||||
// Common spam keywords
|
||||
const spamPatterns = [
|
||||
'viagra', 'cialis', 'casino', 'lottery', 'prize', 'winner',
|
||||
'free money', 'buy now', 'click here', 'earn money', 'make money',
|
||||
'investment opportunity', 'bitcoin', 'cryptocurrency', 'forex',
|
||||
'weight loss', 'diet pill', 'enlargement', 'cheap medication'
|
||||
'viagra',
|
||||
'cialis',
|
||||
'casino',
|
||||
'lottery',
|
||||
'prize',
|
||||
'winner',
|
||||
'free money',
|
||||
'buy now',
|
||||
'click here',
|
||||
'earn money',
|
||||
'make money',
|
||||
'investment opportunity',
|
||||
'bitcoin',
|
||||
'cryptocurrency',
|
||||
'forex',
|
||||
'weight loss',
|
||||
'diet pill',
|
||||
'enlargement',
|
||||
'cheap medication',
|
||||
];
|
||||
|
||||
|
||||
// Check for spam keywords in content
|
||||
if (spamPatterns.some(pattern => lowerContent.includes(pattern))) {
|
||||
if (spamPatterns.some((pattern) => lowerContent.includes(pattern))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check for spam keywords in name or email
|
||||
if (spamPatterns.some(pattern => lowerName.includes(pattern) || lowerEmail.includes(pattern))) {
|
||||
if (spamPatterns.some((pattern) => lowerName.includes(pattern) || lowerEmail.includes(pattern))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check for excessive capitalization (shouting)
|
||||
const uppercaseRatio = (content.match(/[A-Z]/g) || []).length / content.length;
|
||||
if (uppercaseRatio > 0.5 && content.length > 20) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check for excessive special characters
|
||||
const specialChars = "!@#$%^&*()_+-=[]{}\\|;:'\",.<>/?";
|
||||
const specialChars = '!@#$%^&*()_+-=[]{}\\|;:\'",.<>/?';
|
||||
let specialCharCount = 0;
|
||||
|
||||
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
if (specialChars.includes(content[i])) {
|
||||
specialCharCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const specialCharRatio = specialCharCount / content.length;
|
||||
if (specialCharRatio > 0.3 && content.length > 20) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check for excessive URLs - count http:// and https:// occurrences
|
||||
const urlCount = content.split('http').length - 1;
|
||||
if (urlCount > 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -73,34 +88,34 @@ const isSpam = (content: string, name: string, email: string): boolean => {
|
||||
export const GET: APIRoute = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const csrfRequested = url.searchParams.get('csrf') === 'true';
|
||||
|
||||
|
||||
if (csrfRequested) {
|
||||
// Generate and return a CSRF token
|
||||
const csrfToken = generateCsrfToken();
|
||||
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
csrfToken
|
||||
csrfToken,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Default response for GET requests
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message: 'Contact API endpoint is working. Please use POST to submit the form.'
|
||||
message: 'Contact API endpoint is working. Please use POST to submit the form.',
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -108,158 +123,163 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
try {
|
||||
console.log('Contact form submission received');
|
||||
|
||||
|
||||
// Get client IP address for rate limiting
|
||||
const ipAddress = clientAddress || '0.0.0.0';
|
||||
console.log('Client IP:', ipAddress);
|
||||
|
||||
|
||||
// Check rate limit
|
||||
const rateLimitCheck = await checkRateLimit(ipAddress);
|
||||
console.log('Rate limit check:', rateLimitCheck);
|
||||
|
||||
|
||||
if (rateLimitCheck.limited) {
|
||||
console.log('Rate limit exceeded');
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
errors: {
|
||||
rateLimit: rateLimitCheck.message
|
||||
}
|
||||
rateLimit: rateLimitCheck.message,
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Retry-After': '3600'
|
||||
}
|
||||
'Retry-After': '3600',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Get form data
|
||||
const formData = await request.formData();
|
||||
console.log('Form data received');
|
||||
|
||||
|
||||
// Log all form data keys
|
||||
console.log('Form data keys:', [...formData.keys()]);
|
||||
|
||||
|
||||
const name = formData.get('name')?.toString() || '';
|
||||
const email = formData.get('email')?.toString() || '';
|
||||
const message = formData.get('message')?.toString() || '';
|
||||
const disclaimer = formData.get('disclaimer')?.toString() === 'on';
|
||||
const csrfToken = formData.get('csrf_token')?.toString() || '';
|
||||
|
||||
console.log('Form data values:', { name, email, messageLength: message.length, disclaimer, csrfToken: csrfToken ? 'present' : 'missing' });
|
||||
|
||||
|
||||
console.log('Form data values:', {
|
||||
name,
|
||||
email,
|
||||
messageLength: message.length,
|
||||
disclaimer,
|
||||
csrfToken: csrfToken ? 'present' : 'missing',
|
||||
});
|
||||
|
||||
// Get user agent for logging and spam detection
|
||||
const userAgent = request.headers.get('user-agent') || 'Unknown';
|
||||
|
||||
|
||||
// Validate form data
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
|
||||
// Validate CSRF token
|
||||
if (!validateCsrfToken(csrfToken)) {
|
||||
errors.csrf = 'Invalid or expired security token. Please refresh the page and try again.';
|
||||
}
|
||||
|
||||
|
||||
if (!name) {
|
||||
errors.name = 'Please enter your name';
|
||||
} else if (name.length < 2) {
|
||||
errors.name = 'Your name must be at least 2 characters long';
|
||||
}
|
||||
|
||||
|
||||
if (!email) {
|
||||
errors.email = 'Please enter your email address';
|
||||
} else if (!isValidEmail(email)) {
|
||||
errors.email = 'Please enter a valid email address (e.g., name@example.com)';
|
||||
}
|
||||
|
||||
|
||||
if (!message) {
|
||||
errors.message = 'Please enter your message';
|
||||
} else if (message.length < 10) {
|
||||
errors.message = 'Your message must be at least 10 characters long';
|
||||
}
|
||||
|
||||
|
||||
if (!disclaimer) {
|
||||
errors.disclaimer = 'Please check the required consent box before submitting';
|
||||
}
|
||||
|
||||
|
||||
// Check for spam
|
||||
if (isSpam(message, name, email)) {
|
||||
errors.spam = 'Your message was flagged as potential spam. Please revise your message and try again.';
|
||||
}
|
||||
|
||||
|
||||
// If there are validation errors, return them
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
errors
|
||||
errors,
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Send emails
|
||||
console.log('Attempting to send admin notification email');
|
||||
const adminEmailSent = await sendAdminNotification(name, email, message, ipAddress, userAgent);
|
||||
console.log('Admin email sent result:', adminEmailSent);
|
||||
|
||||
|
||||
console.log('Attempting to send user confirmation email');
|
||||
const userEmailSent = await sendUserConfirmation(name, email, message);
|
||||
console.log('User email sent result:', userEmailSent);
|
||||
|
||||
|
||||
// Check if emails were sent successfully
|
||||
if (!adminEmailSent || !userEmailSent) {
|
||||
console.error('Failed to send one or more emails:', { adminEmailSent, userEmailSent });
|
||||
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: 'There was an issue sending your message. Please try again later.'
|
||||
message: 'There was an issue sending your message. Please try again later.',
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Return success response
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: 'Your message has been sent successfully. We will get back to you soon!'
|
||||
message: 'Your message has been sent successfully. We will get back to you soon!',
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing contact form:', error);
|
||||
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
message: 'An error occurred while processing your request. Please try again later.'
|
||||
message: 'An error occurred while processing your request. Please try again later.',
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,32 +1,14 @@
|
||||
---
|
||||
export const prerender = false;
|
||||
import { supportedLanguages } from '~/i18n/translations';
|
||||
|
||||
// Check for language preference in cookies (set by client-side JS)
|
||||
const cookies = Astro.request.headers.get('cookie') || '';
|
||||
const cookieLanguage = cookies.split(';')
|
||||
.map(cookie => cookie.trim())
|
||||
.find(cookie => cookie.startsWith('preferredLanguage='))
|
||||
?.split('=')[1];
|
||||
|
||||
// Get the user's preferred language from the browser if no cookie
|
||||
const acceptLanguage = Astro.request.headers.get('accept-language') || '';
|
||||
// Define the type for supported languages
|
||||
type SupportedLanguage = typeof supportedLanguages[number];
|
||||
|
||||
// Use cookie language if available, otherwise detect from browser
|
||||
const preferredLanguage =
|
||||
(cookieLanguage && supportedLanguages.includes(cookieLanguage as SupportedLanguage))
|
||||
? cookieLanguage
|
||||
: acceptLanguage
|
||||
.split(',')
|
||||
.map(lang => lang.split(';')[0].trim().substring(0, 2))
|
||||
.find(lang => supportedLanguages.includes(lang as SupportedLanguage)) || 'en';
|
||||
import { detectPreferredLanguage } from '~/utils/language';
|
||||
|
||||
// Get the hash fragment if present
|
||||
const url = new URL(Astro.request.url);
|
||||
const hash = url.hash;
|
||||
|
||||
// Detect preferred language
|
||||
const preferredLanguage = detectPreferredLanguage(Astro.request);
|
||||
|
||||
// Redirect to the language-specific homepage
|
||||
return Astro.redirect(`/${preferredLanguage}/${hash}`);
|
||||
---
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user