52 |
--------------------------------------------------------------------------------
/src/i18n/utils.ts:
--------------------------------------------------------------------------------
1 | import { ui } from "./ui";
2 |
3 | export const DEFAULT_LOCALE = "nl";
4 |
5 | export const LOCALES = {
6 | en: "en-US",
7 | nl: "be-nl",
8 | fr: "be-fr",
9 | };
10 |
11 | export type UiType = keyof typeof ui;
12 |
13 | export function getLangFromUrl(url: URL): UiType {
14 | const [, lang] = url.pathname.split("/");
15 | if (lang in ui) return lang as UiType;
16 | return DEFAULT_LOCALE as UiType;
17 | }
18 |
19 | export function useTranslations(lang?: UiType) {
20 | return function t(
21 | key: keyof (typeof ui)[typeof DEFAULT_LOCALE],
22 | ...args: any[]
23 | ) {
24 | let translation =
25 | ui[lang ?? DEFAULT_LOCALE][key] || ui[DEFAULT_LOCALE][key];
26 | if (args.length > 0) {
27 | for (let i = 0; i < args.length; i++) {
28 | translation = translation.replace(`{${i}}`, args[i]);
29 | }
30 | }
31 | return translation;
32 | };
33 | }
34 |
35 | export function pathNameIsInLanguage(pathname: string, lang: UiType): boolean {
36 | return (
37 | pathname.startsWith(`/${lang}`) ||
38 | (lang === DEFAULT_LOCALE && !pathNameStartsWithLanguage(pathname))
39 | );
40 | }
41 |
42 | function pathNameStartsWithLanguage(pathname: string): boolean {
43 | const languages = Object.keys(ui);
44 | for (let i = 0; i < languages.length; i++) {
45 | const lang = languages[i];
46 | if (pathname.startsWith(`/${lang}`)) {
47 | return true;
48 | }
49 | }
50 | return false;
51 | }
52 |
53 | export function getLocalizedPathname(pathname: string, lang: string): string {
54 | if (pathNameStartsWithLanguage(pathname)) {
55 | const availableLanguages = Object.keys(ui).join("|");
56 | const regex = new RegExp(`^\/(${availableLanguages})`);
57 | return pathname.replace(regex, `/${lang}`);
58 | }
59 | return `/${lang}${pathname}`;
60 | }
61 |
--------------------------------------------------------------------------------
/tailwind.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
4 | theme: {
5 | container: {
6 | center: true,
7 | padding: {
8 | DEFAULT: "1.25rem",
9 | sm: "4rem",
10 | lg: "5rem",
11 | xl: "6rem",
12 | "2xl": "8rem",
13 | },
14 | screens: {
15 | DEFAULT: "640px",
16 | sm: "768px",
17 | lg: "1024px",
18 | xl: "1280px",
19 | "2xl": "1280px",
20 | },
21 | },
22 | extend: {
23 | fontFamily: {
24 | raleway: ["Raleway", "sans-serif"],
25 | "raleway-italic": ["Raleway-Italic", "sans-serif"],
26 | "open-sans": ["OpenSans", "sans-serif"],
27 | "open-sans-italic": ["OpenSans-Italic", "sans-serif"],
28 | },
29 | colors: {
30 | white: "#FFFFFF",
31 | accent: "#2CA02C",
32 | gray: "#13151a",
33 | elevated: "#fbf7f5",
34 | negative: "#E62323",
35 | positive: "#00d255",
36 | primary: "#563edc",
37 | secondary: "#cb29ef",
38 | },
39 | keyframes: {
40 | slider: {
41 | from: { transform: "translate(0)" },
42 | to: { transform: "translate(calc(-320px * 5 - 12rem))" },
43 | },
44 | "slider-md": {
45 | from: { transform: "translate(0)" },
46 | to: { transform: "translate(calc(-432px * 5 - 12rem))" },
47 | },
48 | "slider-lg": {
49 | from: { transform: "translate(0)" },
50 | to: { transform: "translate(calc(-544px * 5 - 12rem))" },
51 | },
52 | },
53 | animation: {
54 | slider: "slider 20s linear infinite",
55 | "slider-md": "slider-md 20s linear infinite",
56 | "slider-lg": "slider-lg 20s linear infinite",
57 | },
58 | },
59 | },
60 | plugins: [],
61 | };
62 |
--------------------------------------------------------------------------------
/public/icons/mailerSend.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Astro Spark Template
2 |
3 | Astro Spark is a modern starter template optimized for static sites, built using Astro.
4 |
5 | Demo available at https://www.spark.rubenwinant.be/en
6 |
7 | ## Features
8 |
9 | - Astro for blazing-fast static site generation.
10 | - Alpine.js: Lightweight JS for simple interactivity without overhead.
11 | - Tailwind CSS: Utility-first CSS framework for rapid UI development.
12 | - MDX Support: Write components directly within Markdown.
13 | - Sitemap Generator: Automatically generates a sitemap for SEO.
14 | - SEO Optimized: Meta tags, Open Graph, and structured data support.
15 | - Responsive Design: Out-of-the-box mobile responsiveness.
16 | -
17 |
18 | ## Installation
19 |
20 | First, clone the repository and navigate to the project folder:
21 |
22 | ```
23 | git clone https://github.com/Ruben-Winant/astro_spark_template
24 | cd astro_spark_template
25 | ```
26 |
27 | Next, install the necessary dependencies:
28 |
29 | ```
30 | npm install
31 | ```
32 |
33 | ## Usage
34 |
35 | To start the development server, run:
36 |
37 | ```
38 | npm run dev
39 | ```
40 |
41 | This will launch a local development server accessible at `http://localhost:3000`, allowing you to see changes in real-time.
42 |
43 | To build the project for production:
44 |
45 | ```
46 | npm run build
47 | ```
48 |
49 | This will create an optimized static site in the `dist/` folder.
50 |
51 | ## Scripts
52 |
53 | - `npm run dev`: Starts the development server.
54 | - `npm run build`: Builds the project for production.
55 | - `npm run preview`: Previews the production build locally.
56 | - `npm run astro ...`: Use this for custom Astro commands.
57 |
58 | ## Project Structure
59 |
60 | ```
61 | ├── public/ # Static assets (e.g., images, fonts)
62 | ├── src/
63 | │ ├── components/ # Reusable components (e.g., buttons, headers)
64 | │ ├── layouts/ # Layouts for different page types
65 | │ ├── pages/ # Your page content (in .astro or .mdx)
66 | │ ├── styles/ # Global CSS or Tailwind styles
67 | │ └── content/ # MDX content, easily extendable
68 | ├── package.json # Project scripts and dependencies
69 | └── astro.config.mjs # Astro configuration
70 | ```
71 |
72 | - `components/`: Contains reusable components like navigation, buttons, etc.
73 | - `layouts/`: Defines page layouts (e.g., blog post, homepage).
74 | - `pages/`: All your main site pages (e.g., /index.astro, /about.astro).
75 | - `content/`: For MDX-based content management.
76 | - `styles/`: Custom global styles using Tailwind CSS.
77 |
78 | ## Customization
79 |
80 | - Styling: Modify src/styles and tweak Tailwind CSS as needed.
81 | - MDX Pages: Easily add interactive React components within your Markdown using MDX.
82 | - SEO Tags: Adjust meta tags in each page or globally in layouts/.
83 |
84 | ## Contributing
85 |
86 | Contributions, issues, and feature requests are welcome! Feel free to check the issues page.
87 |
--------------------------------------------------------------------------------
/src/components/BaseHead.astro:
--------------------------------------------------------------------------------
1 | ---
2 | // Import the global.css file here so that it is included on
3 | // all pages through the use of the component.
4 | import {
5 | LOCALES,
6 | pathNameIsInLanguage,
7 | DEFAULT_LOCALE,
8 | getLocalizedPathname,
9 | } from "../i18n/utils";
10 |
11 | interface Props {
12 | title: string;
13 | description: string;
14 | image?: string;
15 | }
16 |
17 | const canonicalPathname = pathNameIsInLanguage(
18 | Astro.url.pathname,
19 | DEFAULT_LOCALE,
20 | )
21 | ? Astro.url.pathname
22 | : getLocalizedPathname(Astro.url.pathname, DEFAULT_LOCALE);
23 | const canonicalURL = new URL(canonicalPathname, Astro.site);
24 |
25 | const { title, description, image = "/favicon.svg" } = Astro.props;
26 |
27 | const alternateLang = Object.keys(LOCALES);
28 | const alternateLinks = alternateLang.map((lang: string) => {
29 | return {
30 | href: new URL(getLocalizedPathname(Astro.url.pathname, lang), Astro.site),
31 | hreflang: lang,
32 | };
33 | });
34 | ---
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
52 |
59 |
66 |
73 |
74 |
75 |
76 |
77 |
78 | {
79 | alternateLinks.map(({ href, hreflang }) => (
80 |
81 | ))
82 | }
83 |
84 |
85 | {title}
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/src/content/blog/en/test.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Introduction to MDX and Markdown
3 | date: 21/09/2024
4 | lang: en
5 | slug: Introduction-to-MDX-and-Markdown
6 | author: Ruben Winant
7 | ---
8 |
9 | # {frontmatter.title}
10 |
11 | If you're new to web development or content creation, you might have come across two terms: **Markdown** and **MDX**. In this post, we’ll explain the basics of both and show you how to get started.
12 |
13 | ## What is Markdown?
14 |
15 | Markdown is a lightweight markup language that allows you to format text using plain text syntax. It's widely used for writing documentation, blog posts, and README files.
16 |
17 | Here's an example of Markdown syntax:
18 |
19 | - **Bold text**: `**Bold**`
20 | - *Italic text*: `*Italic*`
21 | - [Links](https://example.com): `[Link text](https://example.com)`
22 | - Headings:
23 | - `# Heading 1`
24 | - `## Heading 2`
25 | - `### Heading 3`
26 |
27 | ### Example
28 |
29 | Here’s a sample Markdown snippet:
30 |
31 | ```Md
32 | # My First Blog Post
33 |
34 | Welcome to my blog! This is an example of a Markdown file. Below is a list:
35 |
36 | - Item 1
37 | - Item 2
38 | - Item 3
39 |
40 | ```
41 |
42 | When rendered, this will look like:
43 |
44 | # My First Blog Post
45 |
46 | Welcome to my blog! This is an example of a Markdown file. Below is a list:
47 |
48 | - Item 1
49 | - Item 2
50 | - Item 3
51 |
52 | ## What is MDX?
53 |
54 | MDX is an extension of Markdown that allows you to embed JSX (JavaScript XML) directly into your Markdown files. This makes it possible to mix standard Markdown content with dynamic components, making it a powerful tool for modern web development.
55 |
56 | However, if you're just starting out, you can use MDX exactly like Markdown without any JSX components. It works just fine with the basics.
57 |
58 | ### Example
59 |
60 | Here’s what a simple MDX file looks like (without custom components):
61 |
62 | ```mdx
63 | ---
64 | title: "My MDX Post"
65 | ---
66 |
67 | # Hello World
68 |
69 | This is an example MDX post. It behaves just like Markdown, but I can later add components if needed.
70 |
71 | Why Use MDX?
72 |
73 | The main advantage of MDX is that it gives you the flexibility to write simple content using Markdown but allows for dynamic content later on. It's commonly used in static site generators like Astro, Next.js, and Gatsby.
74 | Getting Started
75 |
76 | To create a basic MDX post, you just need to:
77 |
78 | Create a .mdx file.
79 | Write Markdown content as usual.
80 | Optionally, add JSX components later when you’re comfortable with the basics.
81 |
82 | That’s all you need to get started with MDX and Markdown. In future posts, we’ll dive deeper into how you can use MDX to create dynamic and interactive blog posts.
83 |
84 | Stay tuned for more tips and tutorials!
85 |
86 | markdown
87 |
88 |
89 | ### Key Elements:
90 | - **Basic Markdown features** like headings, bold, italics, lists, and links.
91 | - **MDX frontmatter** (`---`) for metadata (title, description, etc.).
92 | - No custom components or JSX, ensuring compatibility across different MDX parsers.
93 |
94 | This post covers the basics and will work smoothly without custom components or advanced features. You can use it as a foundation for learning or sharing MDX and Markdown tips.
95 |
96 |
--------------------------------------------------------------------------------
/src/content/blog/nl/test.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Inleiding tot MDX en Markdown
3 | date: 21/09/2024
4 | lang: nl
5 | slug: Inleiding-tot-MDX-en-Markdown
6 | author: Ruben Winant
7 | ---
8 |
9 | # {frontmatter.title}
10 |
11 | Als je nieuw bent in webontwikkeling of contentcreatie, ben je misschien twee termen tegengekomen: **Markdown** en **MDX**. In deze post leggen we de basis van beide uit en laten we zien hoe je aan de slag kunt.
12 |
13 | ## Wat is Markdown?
14 |
15 | Markdown is een lichtgewicht opmaaktaal waarmee je tekst kunt opmaken met de syntaxis van platte tekst. Het wordt veel gebruikt voor het schrijven van documentatie, blog posts en README bestanden.
16 |
17 | Hier is een voorbeeld van Markdown syntaxis:
18 |
19 | - **Bold text**: `**Bold**`
20 | - *Italic text*: `*Italic*`
21 | - [Links](https://example.com): `[Link text](https://example.com)`
22 | - Headings:
23 | - `# Heading 1`
24 | - `## Heading 2`
25 | - `### Heading 3`
26 |
27 | ### Voorbeeld
28 |
29 | Hier is een voorbeeld van een Markdown fragment:
30 |
31 | ```Md
32 | # Mijn eerste blogpost
33 |
34 | Welkom op mijn blog! Dit is een voorbeeld van een Markdown-bestand. Hieronder staat een lijst:
35 |
36 | - Item 1
37 | - Item 2
38 | - Item 3
39 |
40 | ```
41 |
42 | Bij het renderen ziet dit er als volgt uit:
43 |
44 | # Mijn eerste blogpost
45 |
46 | Welkom op mijn blog! Dit is een voorbeeld van een Markdown-bestand. Hieronder staat een lijst:
47 |
48 | - Item 1
49 | - Item 2
50 | - Item 3
51 |
52 | ## Wat is MDX?
53 |
54 | MDX is een uitbreiding van Markdown waarmee je JSX (JavaScript XML) rechtstreeks in je Markdown-bestanden kunt invoegen. Dit maakt het mogelijk om standaard Markdown-inhoud te mengen met dynamische componenten, waardoor het een krachtig hulpmiddel is voor moderne webontwikkeling.
55 |
56 | Als je echter net begint, kun je MDX net als Markdown gebruiken zonder JSX-componenten. Het werkt prima met de basis.
57 |
58 | ### Voorbeeld
59 |
60 | Zo ziet een eenvoudig MDX-bestand eruit (zonder aangepaste componenten):
61 |
62 | ```mdx
63 | ---
64 | title: "Mijn MDX post"
65 | ---
66 |
67 | # Hello World
68 |
69 | Dit is een voorbeeld van een MDX-post. Het gedraagt zich net als Markdown, maar ik kan later componenten toevoegen als dat nodig is.
70 |
71 | Waarom MDX gebruiken?
72 |
73 | Het belangrijkste voordeel van MDX is dat het je de flexibiliteit geeft om eenvoudige inhoud te schrijven met Markdown, maar later dynamische inhoud mogelijk maakt. Het wordt vaak gebruikt in statische site generators zoals Astro, Next.js en Gatsby.
74 | Aan de slag
75 |
76 | Om een MDX basispost te maken, hoef je alleen maar
77 |
78 | Een .mdx bestand aanmaken.
79 | Markdown inhoud schrijven zoals gewoonlijk.
80 | Eventueel kun je later JSX componenten toevoegen als je vertrouwd bent met de basis.
81 |
82 | Dat is alles wat je nodig hebt om aan de slag te gaan met MDX en Markdown. In toekomstige posts zullen we dieper ingaan op hoe je MDX kunt gebruiken om dynamische en interactieve blog posts te maken.
83 |
84 | Blijf kijken voor meer tips en tutorials!
85 | ```
86 |
87 | ### Belangrijkste elementen:
88 | - **Basis Markdown-functies** zoals koppen, vet, cursief, lijsten en links.
89 |
90 | - **MDX frontmatter** (`--`) voor metadata (titel, beschrijving, enz.).
91 |
92 | - Geen aangepaste componenten of JSX, zodat compatibiliteit met verschillende MDX-parsers gegarandeerd is.
93 |
94 | Deze post behandelt de basis en werkt probleemloos zonder aangepaste componenten of geavanceerde functies. Je kunt het gebruiken als basis voor het leren of delen van MDX en Markdown tips.
95 |
--------------------------------------------------------------------------------
/src/content/blog/fr/test.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title : Introduction à MDX et Markdown
3 | date : 21/09/2024
4 | lang : fr
5 | slug : Introduction-à-MDX-et-Markdown
6 | auteur : Ruben Winant
7 | ---
8 |
9 | # {frontmatter.title}
10 |
11 | Si vous êtes novice en matière de développement web ou de création de contenu, vous avez peut-être déjà rencontré deux termes : **Markdown** et **MDX**. Dans ce billet, nous allons vous expliquer les bases de ces deux termes et vous montrer comment commencer.
12 |
13 | ## Qu'est-ce que Markdown ?
14 |
15 | Markdown est un langage de balisage léger qui vous permet de formater du texte avec la syntaxe du texte brut. Il est largement utilisé pour rédiger de la documentation, des articles de blog et des fichiers README.
16 |
17 | Voici un exemple de syntaxe Markdown :
18 |
19 | - **Texte en gras** : `**Bold**`
20 | - *Texte en italique* : `*Italique*`
21 | - Liens](https://example.com) : `[Texte du lien](https://example.com)`
22 | - Rubriques :
23 | - `# Titre 1`
24 | - `## Titre 2`
25 | - `### Titre 3`
26 |
27 | ### Exemple
28 |
29 | Voici un exemple d'extrait Markdown :
30 |
31 | ```Md
32 | # Mon premier article de blog
33 |
34 | Bienvenue sur mon blog ! Voici un exemple de fichier Markdown. Voici une liste :
35 |
36 | - Point 1
37 | - Point 2
38 | - Point 3
39 |
40 | ```
41 |
42 | Lors du rendu, cela ressemble à ceci :
43 |
44 | # Mon premier billet de blog
45 |
46 | Bienvenue sur mon blog ! Voici un exemple de fichier Markdown. Voici une liste :
47 |
48 | - Élément 1
49 | - Point 2
50 | - Élément 3
51 |
52 | ## Qu'est-ce que MDX ?
53 |
54 | MDX est une extension de Markdown qui vous permet d'insérer du JSX (JavaScript XML) directement dans vos fichiers Markdown. Cela vous permet de mélanger du contenu Markdown standard avec des composants dynamiques, ce qui en fait un outil puissant pour le développement web moderne.
55 |
56 | Toutefois, si vous débutez, vous pouvez utiliser MDX comme Markdown sans composants JSX. Cela fonctionne très bien avec les éléments de base.
57 |
58 | ### Exemple
59 |
60 | Voici à quoi ressemble un fichier MDX simple (sans composants personnalisés) :
61 |
62 | ```mdx
63 | ---
64 | title : « Mon billet MDX »
65 | ---
66 |
67 | # Hello World
68 |
69 | Voici un exemple de message MDX. Il se comporte comme du Markdown, mais je peux ajouter des composants plus tard si nécessaire.
70 |
71 | Pourquoi utiliser MDX ?
72 |
73 | Le principal avantage de MDX est qu'il vous donne la flexibilité d'écrire un contenu simple avec Markdown, tout en permettant un contenu dynamique plus tard. Il est souvent utilisé dans les générateurs de sites statiques tels que Astro, Next.js et Gatsby.
74 | Pour commencer
75 |
76 | Pour créer un billet de base MDX, il suffit de
77 |
78 | Créer un fichier .mdx.
79 | Écrire du contenu Markdown comme d'habitude.
80 | Optionnellement, vous pouvez ajouter des composants JSX plus tard si vous êtes familier avec les bases.
81 |
82 | C'est tout ce dont vous avez besoin pour commencer à utiliser MDX et Markdown. Dans les prochains articles, nous verrons plus en détail comment utiliser MDX pour créer des articles de blog dynamiques et interactifs.
83 |
84 | Restez à l'écoute pour d'autres conseils et tutoriels !
85 | ```
86 |
87 | ### Éléments clés :
88 | - **Fonctions de base de Markdown** telles que les titres, le gras, l'italique, les listes et les liens.
89 |
90 | - **Matière frontale MDX** (`--`) pour les métadonnées (titre, description, etc.).
91 |
92 | - Pas de composants personnalisés ni de JSX, ce qui garantit la compatibilité avec les différents analyseurs MDX.
93 |
94 | Ce billet couvre les bases et fonctionne sans problème sans composants personnalisés ni fonctionnalités avancées. Vous pouvez l'utiliser comme base pour apprendre ou partager des astuces MDX et Markdown.
--------------------------------------------------------------------------------
/src/pages/api/contactFormSubmit.ts:
--------------------------------------------------------------------------------
1 | import type { APIRoute } from "astro";
2 | import { MailerSend, Sender, Recipient, EmailParams } from "mailersend";
3 | import { contactOptions } from "../[lang]/contact.astro";
4 | import { DEFAULT_LOCALE, useTranslations, type UiType } from "../../i18n/utils";
5 |
6 | export const POST: APIRoute = async ({ request }) => {
7 | const formData = await request.formData();
8 | const name = formData.get("name");
9 | const email = formData.get("email");
10 | const topic = formData.get("topic");
11 | const message = formData.get("message");
12 | const lang = (formData.get("lang") as UiType) ?? (DEFAULT_LOCALE as UiType);
13 |
14 | const t = useTranslations(lang);
15 |
16 | const EMAIL: string = "example@hotmail.com";
17 | const mailerSend = new MailerSend({
18 | apiKey: import.meta.env.MAILERSEND_API_KEY ?? "",
19 | });
20 | const sentFrom = new Sender(
21 | "sender@hotmail.com",
22 | "Spark",
23 | );
24 | const recipients = [new Recipient(EMAIL, "Ruben Winant")];
25 | const nameRegex = /^[a-zA-Z\s]+$/;
26 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
27 |
28 | // Validation
29 | let errors = {
30 | name: [] as string[],
31 | topic: [] as string[],
32 | email: [] as string[],
33 | message: [] as string[],
34 | general: [] as string[],
35 | };
36 |
37 | if (
38 | !name ||
39 | typeof name !== "string" ||
40 | name.length < 1 ||
41 | !nameRegex.test(name)
42 | ) {
43 | errors.name.push(t("contact.error_name"));
44 | }
45 |
46 | if (
47 | !email ||
48 | typeof email !== "string" ||
49 | email.length < 1 ||
50 | !emailRegex.test(email)
51 | ) {
52 | errors.email.push(t("contact.error_email"));
53 | }
54 |
55 | if (!topic || typeof topic !== "string" || !contactOptions.includes(topic)) {
56 | errors.topic.push(t("contact.error_topic"));
57 | }
58 |
59 | if (!message || typeof message !== "string" || message.length < 1) {
60 | errors.message.push(t("contact.error_message"));
61 | }
62 |
63 | if (
64 | errors.email.length > 0 ||
65 | errors.name.length > 0 ||
66 | errors.message.length > 0 ||
67 | errors.topic.length > 0
68 | ) {
69 | return new Response(JSON.stringify(errors), { status: 400 });
70 | }
71 |
72 | // Sanitize inputs
73 | const sanitizeInput = (input: FormDataEntryValue) =>
74 | input.toString().replace(/<[^>]*>?/gm, "");
75 | const sanitizedData = {
76 | name: sanitizeInput(name as FormDataEntryValue),
77 | email: sanitizeInput(email as FormDataEntryValue),
78 | message: sanitizeInput(message as FormDataEntryValue),
79 | topic: sanitizeInput(topic as FormDataEntryValue),
80 | };
81 |
82 | const emailParams = new EmailParams()
83 | .setFrom(sentFrom)
84 | .setTo(recipients)
85 | .setSubject(sanitizedData.topic)
86 | .setTemplateId("insert template id")
87 | .setVariables([
88 | {
89 | email: EMAIL,
90 | substitutions: [
91 | { var: "from", value: sanitizedData.name },
92 | { var: "fromEmail", value: sanitizedData.email },
93 | { var: "message", value: sanitizedData.message },
94 | ],
95 | },
96 | ]);
97 |
98 | // send mail
99 | try {
100 | const result = await mailerSend.email.send(emailParams);
101 |
102 | if (result.statusCode >= 200 && result.statusCode < 300) {
103 | return new Response(JSON.stringify({ success: true }), {
104 | status: result.statusCode,
105 | });
106 | } else {
107 | return new Response(
108 | JSON.stringify({ message: t("contact.error_general") }),
109 | { status: 400 },
110 | );
111 | }
112 | } catch (error) {
113 | return new Response(
114 | JSON.stringify({ message: t("contact.error_general") }),
115 | { status: 400 },
116 | );
117 | }
118 | };
119 |
--------------------------------------------------------------------------------
/src/components/Navigation.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getLangFromUrl, useTranslations } from "../i18n/utils";
3 |
4 | const lang = getLangFromUrl(Astro.url);
5 | const t = useTranslations(lang);
6 | ---
7 |
8 |
133 |
--------------------------------------------------------------------------------
/src/pages/[lang]/contact.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getLangFromUrl, useTranslations } from "../../i18n/utils";
3 | import Layout from "../../layouts/Layout.astro";
4 |
5 | const lang = getLangFromUrl(Astro.url);
6 | const t = useTranslations(lang);
7 |
8 | export const contactOptions = ["Inquiry", "Feedback", "Other"];
9 | ---
10 |
11 |
12 |
13 |
14 |
190 |
191 |
192 |
193 |
194 |
--------------------------------------------------------------------------------
/src/i18n/ui.ts:
--------------------------------------------------------------------------------
1 | export const ui = {
2 | en: {
3 | "layout.test": "Spark",
4 | "hero.title": " the best minimal astro starter template!",
5 | "hero.description":
6 | "Launch astro projects with ease and style. Minimalist design, maximum functionality. Whether seasoned or new, Spark streamlines your development for stunning astro websites.",
7 | "integration.title": "Integrations",
8 | "integration.description":
9 | "Click on a card below to open the intergation documentation.",
10 | "faq.title": "FAQ",
11 | "faq.why": "Why did you create this template?",
12 | "faq.who": "Why created this template?",
13 | "faq.tech": "What did you use to create this template?",
14 | "faq.reason_why":
15 | "I created this template cause i wanted to learn about astro.",
16 | "faq.reason_who": "This template was made solely by me, Ruben Winant",
17 | "faq.reason_tech":
18 | "I used Astro, Alpine.js, mdx, sitemap, tailwind, vercel and mailersend",
19 | "faq.description": "This is the faq page",
20 | "nav.components": "Components",
21 | "nav.faq": "faq",
22 | "nav.contact": "contact",
23 | "nav.blog": "blog",
24 | "footer.copyright":
25 | "Spark Theme. All rights reserved. This project is independent of and not endorsed or sponsored by Astro or Tailwind Labs.",
26 | "footer.createdBy": "Created by",
27 | "footer.createdBy2":
28 | "with Astro and Tailwind from the beautiful Bierbeek, Belgium.",
29 | "faq.contact": "Couldn't find what your're looking for?",
30 | "faq.contactUs": "Contact us!",
31 | "contact.title": "Contact",
32 | "contact.field_name": "Name",
33 | "contact.field_topic": "Topic",
34 | "contact.field_email": "Email",
35 | "contact.field_message": "Message",
36 | "contact.topic_Inquiry": "Inquiry",
37 | "contact.topic_Feedback": "Feedback",
38 | "contact.topic_Other": "Other",
39 | "contact.submit": "Send",
40 | "contact.description":
41 | "A simple contact page using plain astro and MailerSendto send mails using their api.",
42 | "contact.error_name": "Please enter a valid name.",
43 | "contact.error_message": "Please enter a valid message.",
44 | "contact.error_email": "Please enter a valid email address.",
45 | "contact.error_topic": "Please select a valid topic.",
46 | "contact.success": "Email was sent successfully",
47 | "contact.error_general": "Something went wrong, try again later",
48 | "slider.title": "Everything you need to get started",
49 | "slider.card_1_title": "Zero JavaScript, By Default",
50 | "slider.card_1_description":
51 | "Astro only ships the JavaScript you need and automatically strips away the rest for a faster website.",
52 | "slider.card_2_title": "Interactive when needed",
53 | "slider.card_2_description":
54 | "Alpine is a rugged, minimal tool for composing behavior directly in your markup. Think of it like jQuery for the modern web.",
55 | "slider.card_3_title": "Multi-language Support",
56 | "slider.card_3_description":
57 | "Multi-language support straight out of the box using Astro's built-in i18n capabilities, making your content accessible to a wider audience.",
58 | "slider.card_4_title": "Style with Tailwind CSS",
59 | "slider.card_4_description":
60 | "Style your website directly in your HTML with utility-first CSS classes, making design a breeze.",
61 | "slider.card_5_title": "SEO optimized",
62 | "slider.card_5_description":
63 | "Astro's sitemap integration helps search engines discover your content, improving your site's SEO effortlessly.",
64 | "features.title": "Features",
65 | "components.description":
66 | "Here you'll find a collection of elements, icons and such that are provided by this theme.",
67 | "test.title": "Sample Card",
68 | "test.description":
69 | "This is a placeholder card to demonstrate the basic structure and functionality of an element. You can customize the content and design to suit your specific needs.",
70 | "test.iconTitle": "Icons",
71 | "test.iconDescription":
72 | "Hover over each icon to see it's name. All icons are svg files within the public/icons directory.",
73 | "test.dropdownCardTitle": "Dropdown cards",
74 | "test.dropdownCardDescription": "Powered by alpine.js",
75 | "test.fullWidthBannerTitle": "Full width banner",
76 | "test.fullWidthBannerDescription":
77 | "A full width banner with an optional cta button",
78 | "test.button": "click here",
79 | "test.autoSliderTitle": "Auto slider with TextCards",
80 | "test.autoSliderDescription":
81 | "A no js, css only auto slider where you can pause the animation by hovering over a card. An optional title and description can also be provided.",
82 | "test.containersTitle": "Containers",
83 | "test.cardGridDescription":
84 | "The CardGrid component allows flexible content layout. Provide content through either the 'upper-row' or 'lower-row' slot, and the component will automatically distribute it into columns.",
85 | "test.cardGridTitle": "What's included",
86 | "test.cardGridTitle1": "Contact page",
87 | "test.cardGridDescription1":
88 | "Capture leads and stay connected with a user-friendly contact form, powered by mailersend.",
89 | "test.cardGridTitle2": "Ready-to-go blog",
90 | "test.cardGridDescription2":
91 | "Start blogging using the I18N compatible, MDX powered blog!",
92 | "test.cardGridTitle3": "Easy setup",
93 | "test.cardGridDescription3":
94 | "Get up and running quickly using our premade pages and components.",
95 | "test.cardGridTitle4": "FAQ page",
96 | "test.cardGridDescription4":
97 | "People might have questions, now you can answer them!",
98 | "test.cardGridTitle5": "Speak Their Language",
99 | "test.cardGridDescription5": "I18N built in. Convert more!",
100 | "test.cardsTitle": "Cards",
101 | "test.cardsDescription": "Some of the premade card components, some of t",
102 | "test.cardClass":
103 | "This CSS class .card is designed for a flexible, visually appealing card component with various styles and hover effects.",
104 | "test.iconCardDescription":
105 | "A responsive, interactive list item (
) that functions as an icon card with an image and title, using Tailwind CSS for styling.",
106 | },
107 | nl: {
108 | "layout.test": "Spark",
109 | "hero.title": " het beste minimale Astro starter template!",
110 | "hero.description":
111 | "Start astro projecten eenvoudig en met stijl. Minimalistisch design, maximale functionaliteit. Of je nu ervaren bent of nieuw, Spark vereenvoudigt je ontwikkeling voor prachtige astro websites.",
112 | "integration.title": "Integraties",
113 | "integration.description":
114 | "Klik op een kaart hieronder om de documentatie van de integratie te openen.",
115 | "faq.title": "Veelgestelde vragen",
116 | "faq.why": "Waarom heb je deze template gemaakt?",
117 | "faq.who": "Wie heeft deze template gemaakt?",
118 | "faq.tech": "Wat heb je gebruikt om deze template te maken?",
119 | "faq.reason_why":
120 | "Ik heb deze template gemaakt omdat ik meer wilde leren over Astro.",
121 | "faq.reason_who":
122 | "Deze template is volledig gemaakt door mij, Ruben Winant",
123 | "faq.reason_tech":
124 | "Ik heb Astro, Alpine.js, mdx, sitemap, tailwind, vercel en mailersend gebruikt",
125 | "faq.description": "Dit is de FAQ-pagina",
126 | "nav.components": "componenten",
127 | "nav.faq": "faq",
128 | "nav.contact": "contact",
129 | "nav.blog": "blog",
130 | "footer.copyright":
131 | "Spark Thema. Alle rechten voorbehouden. Dit project is onafhankelijk van en wordt niet onderschreven of gesponsord door Astro of Tailwind Labs.",
132 | "footer.createdBy": "Gemaakt door",
133 | "footer.createdBy2":
134 | "met Astro en Tailwind vanuit het mooie Bierbeek, België.",
135 | "faq.contact": "Kon je niet vinden wat je zoekt?",
136 | "faq.contactUs": "Contacteer ons!",
137 | "contact.title": "Contact",
138 | "contact.field_name": "Naam",
139 | "contact.field_topic": "Onderwerp",
140 | "contact.field_email": "Email",
141 | "contact.field_message": "Bericht",
142 | "contact.topic_Inquiry": "Vraag",
143 | "contact.topic_Feedback": "Feedback",
144 | "contact.topic_Other": "Overig",
145 | "contact.submit": "Verzenden",
146 | "contact.description":
147 | "Een eenvoudige contactpagina die gebruik maakt van plain astro en MailerSendom mails te versturen via hun api.",
148 | "contact.error_name": "Voer een geldige naam in.",
149 | "contact.error_message": "Voer een geldig bericht in.",
150 | "contact.error_email": "Voer een geldig e-mailadres in.",
151 | "contact.error_topic": "Selecteer een geldig onderwerp.",
152 | "contact.success": "E-mail is succesvol verzonden",
153 | "contact.error_general": "Er ging iets mis, probeer het later nog eens",
154 | "features.title": "Kenmerken",
155 | "slider.title": "Alles wat je nodig hebt om aan de slag te gaan",
156 | "slider.card_1_title": "Nul JavaScript, standaard",
157 | "slider.card_1_description":
158 | "Astro levert alleen de JavaScript die je nodig hebt en stript de rest automatisch weg voor een snellere website.",
159 | "slider.card_2_title": "Interactief wanneer nodig",
160 | "slider.card_2_description":
161 | "Alpine is een robuuste, minimale tool voor het samenstellen van gedrag direct in je markup. Zie het als jQuery voor het moderne web.",
162 | "slider.card_3_title": "Ondersteuning voor meerdere talen.",
163 | "slider.card_3_description":
164 | "Ondersteuning voor meerdere talen direct uit de doos met behulp van Astro's ingebouwde i18n-mogelijkheden, waardoor uw inhoud toegankelijk wordt voor een breder publiek.",
165 | "slider.card_4_title": "Stijl met Tailwind CSS",
166 | "slider.card_4_description":
167 | "Style uw website direct in uw HTML met utility-first CSS-klassen, waardoor ontwerpen een koud kunstje wordt.",
168 | "slider.card_5_title": "SEO geoptimaliseerd",
169 | "slider.card_5_description":
170 | "Astro's sitemap-integratie helpt zoekmachines uw inhoud te ontdekken, waardoor de SEO van uw site moeiteloos wordt verbeterd.",
171 | "components.description":
172 | "Hier vindt u een verzameling elementen, pictogrammen en dergelijke die door dit thema worden geleverd.",
173 | "test.title": "Voorbeeldkaart",
174 | "test.description":
175 | "Dit is een tijdelijke kaart om de basisstructuur en functionaliteit van een element te demonstreren. U kunt de inhoud en het ontwerp aanpassen aan uw specifieke behoeften.",
176 | "test.iconTitle": "Pictogrammen",
177 | "test.iconDescription":
178 | "Houd de muiscursor boven elk pictogram om de naam te zien. Alle pictogrammen zijn svg-bestanden in de map public/icons.",
179 | "test.dropdownCardTitle": "Dropdown-kaarten",
180 | "test.dropdownCardDescription": "Aangedreven door alpine.js",
181 | "test.fullWidthBannerTitle": "Banner op volledige breedte",
182 | "test.fullWidthBannerDescription":
183 | "Een banner op volledige breedte met een optionele call-to-action-knop",
184 | "test.button": "Klik hier",
185 | "test.autoSliderTitle": "Automatische schuifregelaar met tekstkaarten",
186 | "test.autoSliderDescription":
187 | "Een automatische schuifregelaar zonder JavaScript, alleen met CSS, waarbij u de animatie kunt pauzeren door met de muis over een kaart te gaan. Een optionele titel en beschrijving kunnen ook worden opgegeven.",
188 | "test.containersTitle": "Containers",
189 | "test.cardGridDescription":
190 | "De CardGrid-component biedt een flexibele indeling van de inhoud. Voeg inhoud toe via de 'upper-row' of 'lower-row'-slot, en de component zal deze automatisch in kolommen verdelen.",
191 | "test.cardGridTitle": "Wat is inbegrepen",
192 | "test.cardGridTitle1": "Contactpagina",
193 | "test.cardGridDescription1":
194 | "Verzamel leads en blijf verbonden met een gebruiksvriendelijk contactformulier, aangedreven door mailersend.",
195 | "test.cardGridTitle2": "Klaar-om-te-gaan blog",
196 | "test.cardGridDescription2":
197 | "Begin met bloggen met behulp van de I18N-compatibele, MDX-aangedreven blog!",
198 | "test.cardGridTitle3": "Eenvoudige installatie",
199 | "test.cardGridDescription3":
200 | "Kom snel aan de slag met onze vooraf gemaakte pagina's en componenten.",
201 | "test.cardGridTitle4": "FAQ-pagina",
202 | "test.cardGridDescription4":
203 | "Mensen hebben misschien vragen, nu kunt u ze beantwoorden!",
204 | "test.cardGridTitle5": "Spreek hun taal",
205 | "test.cardGridDescription5": "I18N ingebouwd. Converteer meer!",
206 | "test.cardsTitle": "Kaarten",
207 | "test.cardsDescription":
208 | "Enkele van de vooraf gemaakte kaartcomponenten, enkele van t",
209 | "test.cardClass":
210 | "Deze CSS-klasse .card is ontworpen voor een flexibele, visueel aantrekkelijke kaartcomponent met verschillende stijlen en zweveffecten.",
211 | "test.iconCardDescription":
212 | "Een responsief, interactief lijst item (
) dat functioneert als een pictogramkaart met een afbeelding en titel, met behulp van Tailwind CSS voor styling.",
213 | },
214 | fr: {
215 | "layout.test": "Spark",
216 | "hero.title": " le meilleur modèle de démarrage Astro minimal !",
217 | "hero.description":
218 | "Lancez vos projets astro avec facilité et style. Design minimaliste, fonctionnalité maximale. Que vous soyez expérimenté ou novice, Spark simplifie votre développement pour des sites web astro époustouflants.",
219 | "integration.title": "Intégrations",
220 | "integration.description":
221 | "Cliquez sur une carte ci-dessous pour ouvrir la documentation de l'intégration",
222 | "faq.title": "FAQ",
223 | "faq.why": "Pourquoi avez-vous créé ce modèle?",
224 | "faq.who": "Qui a créé ce modèle?",
225 | "faq.tech": "Qu'avez-vous utilisé pour créer ce modèle?",
226 | "faq.reason_why":
227 | "J'ai créé ce modèle car je voulais apprendre à utiliser Astro.",
228 | "faq.reason_who":
229 | "Ce modèle a été entièrement réalisé par moi-même, Ruben Winant",
230 | "faq.reason_tech":
231 | "J'ai utilisé Astro, Alpine.js, mdx, sitemap, tailwind, vercel et mailersend",
232 | "faq.description": "Ceci est la page des FAQ",
233 | "nav.components": "Composants",
234 | "nav.faq": "faq",
235 | "nav.contact": "contact",
236 | "nav.blog": "blog",
237 | "footer.copyright":
238 | "Spark Theme. Tous droits réservés. Ce projet est indépendant d'Astro ou de Tailwind Labs et n'est ni approuvé ni sponsorisé par eux.",
239 | "footer.createdBy": "Créé par",
240 | "footer.createdBy2":
241 | "avec Astro et Tailwind depuis le magnifique Bierbeek, en Belgique",
242 | "faq.contact": "Vous n'avez pas trouvé ce que vous cherchiez ?",
243 | "faq.contactUs": "Contactez-nous !",
244 | "contact.title": "Contact",
245 | "contact.field_name": "Name",
246 | "contact.field_topic": "Sujet",
247 | "contact.field_email": "Email",
248 | "contact.field_message": "Message",
249 | "contact.topic_Inquiry": "Enquête",
250 | "contact.topic_Feedback": "Feedback",
251 | "contact.topic_Other": "Autre",
252 | "contact.submit": "Envoyer",
253 | "contact.description":
254 | "Une simple page de contact utilisant astro et MailerSendpour envoyer des mails en utilisant leur api.",
255 | "contact.error_name": "Veuillez entrer un nom valide",
256 | "contact.error_message": "Veuillez saisir un message valide.",
257 | "contact.error_email": "Veuillez saisir une adresse électronique valide.",
258 | "contact.error_topic": "Veuillez sélectionner un sujet valide",
259 | "contact.success": "L'e-mail a été envoyé avec succès",
260 | "contact.error_general":
261 | "Quelque chose s'est mal passé, réessayez plus tard",
262 | "features.title": "Features",
263 | "slider.title": "Tout ce dont vous avez besoin pour commencer",
264 | "slider.card_1_title": "Zéro JavaScript, par défaut",
265 | "slider.card_1_description":
266 | "Astro ne fournit que le JavaScript dont vous avez besoin et supprime automatiquement le reste pour un site web plus rapide.",
267 | "slider.card_2_title": "Interactive when needed",
268 | "slider.card_2_description":
269 | "Alpine est un outil robuste et minimal pour composer des comportements directement dans votre balisage. Voyez-le comme jQuery pour le web moderne.",
270 | "slider.card_3_title": "Multi-language Support",
271 | "slider.card_3_description":
272 | "Prise en charge multilingue dès la sortie de la boîte grâce aux capacités i18n intégrées d'Astro, ce qui rend votre contenu accessible à un public plus large.",
273 | "slider.card_4_title": "Style with Tailwind CSS",
274 | "slider.card_4_description":
275 | "Stylez votre site web directement dans votre HTML avec des classes CSS utilitaires, ce qui facilite la conception.",
276 | "slider.card_5_title": "SEO optimized",
277 | "slider.card_5_description":
278 | "L'intégration du sitemap d'Astro aide les moteurs de recherche à découvrir votre contenu, améliorant ainsi sans effort le référencement de votre site.",
279 | "components.description":
280 | "Vous trouverez ici une collection d'éléments, d'icônes et autres qui sont fournis par ce thème.",
281 | "test.title": "Sample Card",
282 | "test.description":
283 | "This is a placeholder card to demonstrate the basic structure and functionality of an element. You can customize the content and design to suit your specific needs.",
284 | "test.iconTitle": "Icônes",
285 | "test.iconDescription":
286 | "Survolez chaque icône pour voir son nom. Toutes les icônes sont des fichiers svg dans le répertoire public/icons.",
287 | "test.dropdownCardTitle": "Cartes déroulantes",
288 | "test.dropdownCardDescription": "Alimenté par alpine.js",
289 | "test.fullWidthBannerTitle": "Bannière pleine largeur",
290 | "test.fullWidthBannerDescription":
291 | "Une bannière pleine largeur avec un bouton d'appel à l'action optionnel",
292 | "test.button": "Cliquez ici",
293 | "test.autoSliderTitle": "Curseur automatique avec cartes de texte",
294 | "test.autoSliderDescription":
295 | "Un curseur automatique sans JavaScript, uniquement avec CSS, où vous pouvez mettre en pause l'animation en survolant une carte. Un titre et une description facultatifs peuvent également être fournis.",
296 | "test.containersTitle": "Conteneurs",
297 | "test.cardGridDescription":
298 | "Le composant CardGrid permet une disposition flexible du contenu. Fournissez du contenu via le slot 'upper-row' ou 'lower-row', et le composant le distribuera automatiquement en colonnes.",
299 | "test.cardGridTitle": "Ce qui est inclus",
300 | "test.cardGridTitle1": "Page de contact",
301 | "test.cardGridDescription1":
302 | "Capturez des leads et restez connecté avec un formulaire de contact convivial, alimenté par mailersend.",
303 | "test.cardGridTitle2": "Blog prêt à l'emploi",
304 | "test.cardGridDescription2":
305 | "Commencez à bloguer en utilisant le blog compatible I18N, alimenté par MDX !",
306 | "test.cardGridTitle3": "Installation facile",
307 | "test.cardGridDescription3":
308 | "Soyez opérationnel rapidement en utilisant nos pages et composants pré-faits.",
309 | "test.cardGridTitle4": "Page FAQ",
310 | "test.cardGridDescription4":
311 | "Les gens peuvent avoir des questions, maintenant vous pouvez y répondre !",
312 | "test.cardGridTitle5": "Parlez leur langue",
313 | "test.cardGridDescription5": "I18N intégré. Convertissez plus !",
314 | "test.cardsTitle": "Cartes",
315 | "test.cardsDescription":
316 | "Quelques-uns des composants de carte pré-faits, quelques-uns de t",
317 | "test.cardClass":
318 | "Cette classe CSS .card est conçue pour un composant de carte flexible et visuellement attrayant avec différents styles et effets de survol.",
319 | "test.iconCardDescription":
320 | "Un élément de liste (
) réactif et interactif qui fonctionne comme une carte d'icône avec une image et un titre, en utilisant Tailwind CSS pour le style.",
321 | },
322 | };
323 |
--------------------------------------------------------------------------------