+
Blog
Web App
diff --git a/src/lib/data/blog-posts/index.ts b/src/lib/data/blog-posts/index.ts
new file mode 100644
index 0000000..73fa19f
--- /dev/null
+++ b/src/lib/data/blog-posts/index.ts
@@ -0,0 +1,4 @@
+import { filterPosts, importPosts } from './utils';
+
+export const allPosts = importPosts(true);
+export const filteredPosts = filterPosts(allPosts);
\ No newline at end of file
diff --git a/src/lib/data/blog-posts/utils.ts b/src/lib/data/blog-posts/utils.ts
new file mode 100644
index 0000000..c2b8bc1
--- /dev/null
+++ b/src/lib/data/blog-posts/utils.ts
@@ -0,0 +1,73 @@
+// Disabling eslint because importing Prism is needed
+// even if not directly used in this file
+// eslint-disable-next-line no-unused-vars
+import Prism from 'prismjs';
+// Here we assign it to a variable so the import above
+// is not removed automatically on build
+const ifYouRemoveMeTheBuildFails = Prism;
+import 'prism-svelte';
+import readingTime from 'reading-time';
+import striptags from 'striptags';
+import type { BlogPost } from '$lib/utils/types';
+
+export const importPosts = (render = false) => {
+ const blogImports = import.meta.glob('$routes/*/*/*.md', { eager: true });
+ const innerImports = import.meta.glob('$routes/*/*/*/*.md', { eager: true });
+
+ const imports = { ...blogImports, ...innerImports };
+
+ const posts: BlogPost[] = [];
+ for (const path in imports) {
+ const post = imports[path] as any;
+ if (post) {
+ posts.push({
+ ...post.metadata,
+ html: render && post.default.render ? post.default.render()?.html : undefined
+ });
+ }
+ }
+
+ return posts;
+};
+
+export const filterPosts = (posts: BlogPost[]) => {
+ return posts
+ .filter((post) => !post.hidden)
+ .sort((a, b) =>
+ new Date(a.date).getTime() > new Date(b.date).getTime()
+ ? -1
+ : new Date(a.date).getTime() < new Date(b.date).getTime()
+ ? 1
+ : 0
+ )
+ .map((post) => {
+ const readingTimeResult = post.html ? readingTime(striptags(post.html) || '') : undefined;
+ const relatedPosts = getRelatedPosts(posts, post);
+
+ return {
+ ...post,
+ readingTime: readingTimeResult ? readingTimeResult.text : '',
+ relatedPosts: relatedPosts
+ } as BlogPost;
+ });
+};
+
+// #region Unexported Functions
+
+const getRelatedPosts = (posts: BlogPost[], post: BlogPost) => {
+ // Get the first 3 posts that have the highest number of tags in common
+ const relatedPosts = posts
+ .filter((p) => !p.hidden && p.slug !== post.slug)
+ .sort((a, b) => {
+ const aTags = a.tags?.filter((t) => post.tags?.includes(t));
+ const bTags = b.tags?.filter((t) => post.tags?.includes(t));
+ return aTags?.length > bTags?.length ? -1 : aTags?.length < bTags?.length ? 1 : 0;
+ });
+
+ return relatedPosts.slice(0, 3).map((p) => ({
+ ...p,
+ readingTime: p.html ? readingTime(striptags(p.html) || '').text : ''
+ }));
+};
+
+// #endregion
diff --git a/src/lib/data/meta.ts b/src/lib/data/meta.ts
index 2268aea..9c1ebe2 100644
--- a/src/lib/data/meta.ts
+++ b/src/lib/data/meta.ts
@@ -219,5 +219,6 @@ export const keywords = [
'lingua scambio in Faroese', // 'language exchange in Faroese' in Faroese
'lingua scambio in Icelandic', // 'language exchange in Icelandic' in Icelandic
'lingua scambio in Greenlandic', // 'language exchange in Greenlandic' in Greenlandic
- 'lingua scambio in Sami' // 'language exchange in Sami' in Sami
+ 'lingua scambio in Sami', // 'language exchange in Sami' in Sami
+ 'blog'
];
diff --git a/src/lib/utils/types.ts b/src/lib/utils/types.ts
index 5b82b1f..cdcc12b 100644
--- a/src/lib/utils/types.ts
+++ b/src/lib/utils/types.ts
@@ -21,3 +21,18 @@ export type Feature = {
image: string;
tags: TagType[];
};
+
+export type BlogPost = {
+ tags: string[];
+ keywords: string[];
+ hidden: boolean;
+ slug: string;
+ title: string;
+ date: string;
+ updated: string;
+ excerpt: string;
+ html: string | undefined;
+ readingTime: string;
+ relatedPosts: BlogPost[];
+ coverImage: string | undefined;
+};
diff --git a/src/routes/(blog-article)/+layout.server.ts b/src/routes/(blog-article)/+layout.server.ts
new file mode 100644
index 0000000..de03200
--- /dev/null
+++ b/src/routes/(blog-article)/+layout.server.ts
@@ -0,0 +1,11 @@
+import { filteredPosts } from '$lib/data/blog-posts';
+
+export async function load({ url }: { url: { pathname: string } }) {
+ const { pathname } = url;
+ const slug = pathname.replace('/', '');
+ const post = filteredPosts.find((post) => post.slug === slug);
+
+ return {
+ post
+ };
+}
diff --git a/src/routes/(blog-article)/+layout.svelte b/src/routes/(blog-article)/+layout.svelte
new file mode 100644
index 0000000..460ba07
--- /dev/null
+++ b/src/routes/(blog-article)/+layout.svelte
@@ -0,0 +1,190 @@
+
+
+
+ {#if post}
+
+
+
+
+
+
+
+ {post.title} - {title}
+
+
+
+ {#if post.coverImage}
+
+
+ {/if}
+ {/if}
+
+
+
+
+
+
+
+
+ {#if post && post.coverImage}
+
+
+
+ {/if}
+
+
+
+
+
+ {#if post.relatedPosts && post.relatedPosts.length > 0}
+
+
+
+ {/if}
+
+
+
+
+
+
diff --git a/src/routes/(blog-article)/blog-posts/+page.md b/src/routes/(blog-article)/blog-posts/+page.md
new file mode 100644
index 0000000..d127dfa
--- /dev/null
+++ b/src/routes/(blog-article)/blog-posts/+page.md
@@ -0,0 +1,67 @@
+---
+title: How Blog Posts Work
+slug: blog-posts
+coverImage: /images/posts/blog-posts.jpg
+date: 2024-02-26T23:55:15.361Z
+excerpt: How to manage existing blog posts and create new ones
+tags:
+ - Documentation
+---
+
+
+
+All blog posts are located inside the `src/routes/(blog-article)` folder. Each folder inside it represents a blog post, and each folder has a `+page.md` file, which is the file that contains the post's content.
+
+This way, the URL for each blog post is generated with the folder's name. For example, the folder `src/routes/(blog-article)/how-blog-posts-work` will generate the URL `https://mysite.com/how-blog-posts-work`.
+
+All posts are Markdown files, which means you can use the [Markdown syntax](https://www.markdownguide.org/basic-syntax) in them, and it will work out of the box. However, since this projects uses [MDsveX](https://mdsvex.pngwn.io/) to parse Markdown, you can also use Svelte components inside them! This means that the components used in other pages can also be used in blog posts.
+
+
+ This is a Svelte component inside a Markdown file!
+
+
+## Processing
+
+Besides the blog post page itself, the blog posts can be displayed in other places, such as the `/blog` page, which lists all blog posts, and the `
` component, used in the home page.
+
+To be able to do that, posts are processed in the `src/lib/data/blog-posts/index.ts` file. That file imports the blog post files and processes them, so we're able to use some of the post's metadata to list them. For example, we get the post's title, cover image, and calculate the reading time based on its content, so that information is displayed in the blog post cards in the `/blog` page.
+
+There is also some basic logic to get related posts based on a post's tags. The logic should be straightforward enough to modify it to your needs.
+
+## Creating a new post
+
+To create a new post, create a new folder inside the `src/routes/(blog-article)` folder, and inside it, create a `+page.md` file. The folder name will be used as the post's URL slug, so make sure it's a valid URL slug.
+
+Inside the `+page.md` file, you must start with the front matter, which is a YAML-like syntax that is used to define metadata for the post. The front matter must be the first thing in the file, and must be separated from the rest of the content by three dashes (`---`). An example of a front matter is:
+
+
+
+```md
+---
+slug: my-new-blog-post
+title: My New Blog Post
+date: 2023-04-22T20:45:25.350Z
+excerpt: A short description of the post
+coverImage: /images/posts/cover-image.jpg
+tags:
+ - Example
+---
+```
+
+
+
+## Managing blog posts
+
+I highly recommend the [Front Matter VS Code extension](https://frontmatter.codes/) to manage blog posts. It gives you a nice CMS-like UI to manage the front matter of all blog posts, as well as a preview of their content. It is, of course, optional, and you can manage everything directly in the Markdown files if you prefer.
+
+
+
+
+
+## RSS
+
+This template automatically generates a RSS feed of your blog posts. It is generated in the `src/routes/rss.xml/+server.ts` file, and it is available at the `/rss.xml` URL.
diff --git a/src/routes/(blog-article)/customization/+page.md b/src/routes/(blog-article)/customization/+page.md
new file mode 100644
index 0000000..359af61
--- /dev/null
+++ b/src/routes/(blog-article)/customization/+page.md
@@ -0,0 +1,60 @@
+---
+slug: customization
+title: How to Customize this Template
+date: 2024-02-26T21:55:27.154Z
+excerpt: How to customize what you're seeing here and make it your own.
+coverImage: /images/posts/customization.jpg
+tags:
+ - Documentation
+---
+
+In general, content can be modified by editing the **organisms** and the pages themselves. Below is a list of the most common changes that you may want to make.
+
+## Domain/site URL
+
+The first thing you might want to do is replace the domain of of the site with your own. There are two places where you need to do that:
+
+- In the `package.json` file, check the `postbuild` script. Change the domain there to your own, so it ends up like this: `svelte-sitemap --domain https://your-domain.com`. This is used to generate the sitemap of your website, which is used by search engines to index your site.
+- In the `src/lib/data/meta.ts` file, change the `siteBaseUrl` property to your own domain. This is used in multiple parts of the app wherever the site needs to link to itself.
+
+## Header/site logo
+
+To replace the logo that appears in the header, modify or replace the contents of the `Logo.svelte` atom.
+
+The links that appear on the header can be modified directly in the `Header.svelte` organism.
+
+## Hero section
+
+The hero section is the first section of the site's home page. It is composed of a Heading, the _intro_ text, and a list of buttons/CTAs. The contents of this section can be modified directly in the `Hero.svelte` organism.
+
+## About section
+
+The about section contains another headline, a paragraph of text, some social media links, and optionally an image. The contents of this section can be modified directly in the `About.svelte` organism.
+
+## Social Links
+
+The social links are contained in the `Socials.svelte` molecule and can be used in any page. This template shows them on the About section and in the Footer.
+
+## Footer
+
+The footer contains some credits, a list of social links, and the RSS/Theme toggle. The contents of this section can be modified directly in the `Footer.svelte` organism.
+
+## Colors
+
+You can change the color palette of the website just by tweaking the `_themes.scss` file. The file uses the `define-color` scss function to automatically set the color variables in Hex, RGB and HSL formats, so you can choose whichever format you need.
+
+The main theme colors (primary and secondary) have two variants: shade and tint. The shade is a lighter version of the color (darker in dark mode), and the tint should almost match the page's background, so that in light mode, it's really bright, and in dark mode, it's really dark.
+
+## Fonts
+
+This template uses the Inter, Merriweather and Ubuntu Mono font families. You can change the font family by editing the `_variables.scss` file, and the code is already set up to accept a default font, a heading font, and a monospace font.
+
+I recommend using [Fontsource](https://fontsource.org/) to import the fonts you need, or self-hosting them. In case you're using Fontsource, you can import the fonts in `global.scss` file to make sure they're available in the entire site.
+
+## Favicon
+
+Favicons are located in the `static/favicons` folder. I like to use [Real Favicon Generator](https://realfavicongenerator.net) to generate mine, but you can use any other tool you like. I wrote [a blog post about Favicons](https://fantinel.dev/fixing-favicons) in case you want to learn more about them.
+
+## Social Media Link Preview
+
+You probably want to customize how links to your website look when posted on social media/messaging apps. To do that, you can edit the info in `src/lib/data/meta.ts`. There, you can edit the site's title, description, tags (used by search engines) and the image that appears when sharing a link.
\ No newline at end of file
diff --git a/src/routes/(blog-article)/open-source-alternative-to-tandem/+page.md b/src/routes/(blog-article)/open-source-alternative-to-tandem/+page.md
new file mode 100644
index 0000000..a27780d
--- /dev/null
+++ b/src/routes/(blog-article)/open-source-alternative-to-tandem/+page.md
@@ -0,0 +1,72 @@
+---
+slug: open-source-alternative-to-tandem
+title: 100% Open Source Alternative to Tandem
+date: 2024-02-27T11:15:21.800Z
+excerpt: We've been working on an exciting project for the almost past year.
+coverImage: /images/posts/featured_screenshots.jpeg
+tags:
+ - Open Source
+ - Language Exchange
+ - Tandem
+ - App Launch
+---
+
+
+
+# A New and Simple Language Exchange App
+
+We're excited to announce the launch of languageXchange, a 100% free and open sourced language exchange app, best alternative of Tandem. The app is designed to help language learners connect with native speakers of the language they're learning.
+
+## How It Works
+
+languageXchange is simple and straightforward to use. After signing up, you'll be asked to select your native language and the language you're learning. You'll then be matched with native speakers of your target language who are learning your native language.
+
+
+ languageXchange is a great way to practice a new language with native speakers!
+
+
+
+
+## Features
+
+We offer a range of features to help you get the most out of your language learning experience:
+
+- **⚙️ Fine Tune Your Connections** : Customize your connection preferences to find the perfect language exchange partners by filtering options.
+
+- **🔍 Profile Insights** : Get insights into your language learning progress and habits directly from your profile.
+
+- **💬 Just Chat** : Experience our user-friendly chat interface. Learning a language has never been this fun and easy.
+
+- **🔒 Your Data, Your Privacy** : We respect your privacy. Control what data you share and manage your privacy settings easily.
+
+- **⭐ Rating Evaluation** : Rate your language exchange partners and receive ratings to help improve the quality of interactions in our community. _coming-soon_
+
+- **🌙 Night Mode Engage** : Switch to night mode for a more comfortable reading experience in low light environments.
+
+- **🏅 Badge** : Earn badges for your achievements and display them on your profile.
+
+- **💰 Zero Cost** : Experience our comprehensive language learning features at zero cost. Absolutely no hidden charges or in-app purchases.
+
+- **📖 100% Open-Sourced** : Our app is completely open-sourced. Join our developer community and contribute to our codebase.
+
+And, The Most Exciting one is that
+
+- **🤖 Learn with AI** : Harness the power of AI to accelerate your language learning journey. Get personalized feedback. It is going to be your private language copilot, it feedbacks you when you are practicing with a real person in a room. You are going to be so excited what it can speed up your actively language learning process. It's of course powered by ChatGPT. Stay tuned! _coming-soon_
+
+## Be a Contributor
+
+We're always looking for contributors to help improve languageXchange. If you're interested in contributing, join our Discord server and check out the project on GitHub.
+
+
+ Whatever you can contribute, whether it's code, design, or translations, marketing, and so on, we'd love to have you on board.
+
+
+Additionally, we're looking for language exchange partners to help test the app. To get started, simply sign up and create a profile. You'll then be able to start connecting with language exchange partners.
+
+## Conclusion
+
+We're excited to launch languageXchange and look forward to helping language learners connect with native speakers. To stay updated on the latest languageXchange news and updates, be sure to follow us on [Discord](https://discord.gg/CpDZ3kg2rJ).
diff --git a/src/routes/(blog-article)/project-structure/+page.md b/src/routes/(blog-article)/project-structure/+page.md
new file mode 100644
index 0000000..db92c88
--- /dev/null
+++ b/src/routes/(blog-article)/project-structure/+page.md
@@ -0,0 +1,57 @@
+---
+slug: project-structure
+title: Project Structure
+date: 2024-02-26T21:55:21.800Z
+excerpt: How code and structure are organized.
+coverImage: /images/posts/project-structure.jpg
+tags:
+ - Documentation
+---
+
+
+
+This project follows the basic [SvelteKit structure](https://kit.svelte.dev/docs/project-structure), with some added conventions to make customization a post-management easier.
+
+## Components
+
+The components are organized following the [Atomic Design](https://medium.com/@WeAreMobile1st/atomic-design-getting-started-916bc81bad0e) principles, albeit somewhat simplified. Components are in the `src/lib/components` folder, and are organized in the following way:
+
+### Atoms
+
+Atoms are the most basic components, which can be seen as building blocks for other components. They're small and self-contained, and do only one thing. Examples of atoms are buttons, inputs, and tags.
+
+### Molecules
+
+Molecules are groups of atoms that work together to form a more complex component. Examples of molecules are cards, groups of links, or an alert callout.
+
+### Organisms
+
+Organisms, in this project, are code blocks that represent a section of a page, such as the header, footer and hero section. They can be used directly in a page as a sort of building block, so the page's code can be as simple as this:
+
+
+
+```html
+
+
+
+
+```
+
+
+
+## Component Gallery
+
+This project uses [Histoire](https://histoire.dev) to be able to see and develop components in isolation. To open it, run `npm run story:dev`. This way you can customize and build new components with placeholder content and without worrying about where to put them in a page.
+
+## Pages
+
+Pages obey the default SvelteKit structure, but can be summarized as follows:
+
+- There are two different file types: the pages (`+page.svelte`) and the layouts (`+layout.svelte`). Layouts are a way to reuse the same structure between different pages without having to duplicate code;
+- There are two different page layouts in this site: the "Waves" layout, which all pages inside the `(waves)` folder use, and the "Blog Article" layout, which all pages inside the `(blog-article)` folder use;
+
+## Blog Posts
+
+To know how blog posts work and how to create new ones, check out [How Blog Posts Work](/blog-posts).
\ No newline at end of file
diff --git a/src/routes/(waves)/+page.server.ts b/src/routes/(waves)/+page.server.ts
index c2d6fcd..7c2ce35 100644
--- a/src/routes/(waves)/+page.server.ts
+++ b/src/routes/(waves)/+page.server.ts
@@ -1,7 +1,11 @@
import features from '$lib/data/features';
+import { filteredPosts } from '$lib/data/blog-posts';
export async function load() {
- return {
- features
- };
+ const posts = filteredPosts.slice(0, 4);
+
+ return {
+ features,
+ posts
+ };
}
diff --git a/src/routes/(waves)/+page.svelte b/src/routes/(waves)/+page.svelte
index b3bed6d..70acd9a 100644
--- a/src/routes/(waves)/+page.svelte
+++ b/src/routes/(waves)/+page.svelte
@@ -2,17 +2,22 @@
import Hero from '$lib/components/organisms/Hero.svelte';
import About from '$lib/components/organisms/About.svelte';
import Features from '$lib/components/organisms/Features.svelte';
- import type { Feature } from '$lib/utils/types';
+ import RecentPosts from '$lib/components/organisms/RecentPosts.svelte';
+ import type { Feature, BlogPost } from '$lib/utils/types';
export let data: {
features: Feature[];
+ posts: BlogPost[];
};
- let { features } = data;
+ let { features, posts } = data;
+ {#if posts && posts.length > 0}
+
+ {/if}
diff --git a/src/routes/(waves)/blog/+page.server.ts b/src/routes/(waves)/blog/+page.server.ts
new file mode 100644
index 0000000..455adae
--- /dev/null
+++ b/src/routes/(waves)/blog/+page.server.ts
@@ -0,0 +1,7 @@
+import { filteredPosts } from '$lib/data/blog-posts';
+
+export async function load() {
+ return {
+ posts: filteredPosts
+ };
+}
diff --git a/src/routes/(waves)/blog/+page.svelte b/src/routes/(waves)/blog/+page.svelte
new file mode 100644
index 0000000..2c25a32
--- /dev/null
+++ b/src/routes/(waves)/blog/+page.svelte
@@ -0,0 +1,68 @@
+
+
+
+
+
+ {#each posts as post}
+
+ {/each}
+
+
+
+
+
diff --git a/src/routes/rss.xml/+server.ts b/src/routes/rss.xml/+server.ts
new file mode 100644
index 0000000..9be722c
--- /dev/null
+++ b/src/routes/rss.xml/+server.ts
@@ -0,0 +1,72 @@
+import { description, siteBaseUrl, title } from '$lib/data/meta';
+import type { BlogPost } from '$lib/utils/types';
+import dateformat from 'dateformat';
+import { filterPosts, importPosts } from '$lib/data/blog-posts/utils';
+
+export const prerender = true;
+
+export async function GET() {
+ const allPosts = importPosts(true);
+ const filteredPosts = filterPosts(allPosts);
+
+ const body = xml(filteredPosts);
+ const headers = {
+ 'Cache-Control': 'max-age=0, s-maxage=3600',
+ 'Content-Type': 'application/xml'
+ };
+ return new Response(body, { headers });
+}
+
+const xml = (posts: BlogPost[]) => `
+
+
+
+ ${title}
+ ${siteBaseUrl}
+ ${description}
+
+ ${siteBaseUrl}/favicons/favicon-32x32.png
+ ${title}
+ ${siteBaseUrl}
+ 32
+ 32
+
+ ${posts
+ .map(
+ (post) => `
+ -
+ ${siteBaseUrl}/${post.slug}
+ ${post.title}
+ ${post.excerpt}
+ ${siteBaseUrl}/${post.slug}
+ ${dateformat(post.date, 'ddd, dd mmm yyyy HH:MM:ss o')}
+ ${post.tags ? post.tags.map((tag) => `${tag}`).join('') : ''}
+
+ If anything looks wrong,
+
+
+ read on the site!
+
+
+
+
+ ${post.html}
+ ]]>
+ ${post.coverImage ? `