Add new blog post layout and content, update

project and blog content, and add filter category
component
This commit is contained in:
matthieu42morin 2023-11-02 01:15:04 +01:00
parent 9a5d98b033
commit eb0386884e
15 changed files with 495 additions and 16 deletions

33
src/content/blog.ts Normal file
View File

@ -0,0 +1,33 @@
import type { BlogPost } from '$lib/types/blog';
import type { MarkdownMetadata } from '$lib/contents/types';
import type { MdsvexImport } from './types';
import { parseReadContent } from '../../content/utils';
import { error } from '@sveltejs/kit';
export function listBlogPosts() {
const posts = import.meta.glob<BlogPost>('./blog/*.md', {
eager: true,
import: 'metadata'
});
return parseReadContent(posts);
}
export async function getBlogPostMetadata(slug: string) {
const { post } = await getBlogPost(slug);
return post;
}
export async function getBlogPost(slug: string) {
try {
const data: MdsvexImport<BlogPost & MarkdownMetadata> = await import(`./blog/${slug}.md`);
return {
post: { ...data.metadata, slug },
Component: data.default
};
} catch {
throw error(404, `Unable to find blog post "${slug}"`);
}
}

39
src/content/projects.ts Normal file
View File

@ -0,0 +1,39 @@
import type { MarkdownMetadata, MdsvexImport } from './types';
import { parseReadContent } from './utils';
import { error } from '@sveltejs/kit';
export interface Project extends MarkdownMetadata {
title: string;
excerpt: string;
slug: string;
image: string;
date: string;
pageTitle: string;
pageDescription: string;
keywords: string;
}
/**
* Gets all the projects metadata
*/
export function listProjects() {
const projects = import.meta.glob<Project>('./projects/*.md', {
eager: true,
import: 'metadata'
});
return parseReadContent(projects);
}
export async function getProject(slug: string) {
try {
const data: MdsvexImport<Project> = await import(`./projects/${slug}.md`);
return {
post: { ...data.metadata, slug },
Component: data.default
};
} catch {
throw error(404, `Unable to find project "${slug}"`);
}
}

View File

@ -0,0 +1,18 @@
<script lang="ts">
import PostContentLayout from './post-content-layout.svelte';
import type { BlogPost } from '$lib/types/blog';
export let post: BlogPost;
</script>
<PostContentLayout
{...post}
imagesDirectoryName="blog"
baseUrl="https://www.mattmor.in/blog/"
>
<slot />
</PostContentLayout>

View File

@ -0,0 +1,50 @@
<script lang="ts">
import type { BlogTag } from '$lib/types/blog';
import Button from '$lib/components/ui-library/button/button.svelte';
export let selected: BlogTag;
let className = '';
export { className as class };
import { page } from '$app/stores';
import { goto } from '$app/navigation';
let options: BlogTag[] = [
'Company building',
'Engineering',
'Gitpod updates',
'Developer experience',
];
const clickHandler = (value: BlogTag) => {
if (value === selected) {
goto(`/blog`, { keepFocus: true, noScroll: true });
selected = '';
return;
}
let query = new URLSearchParams($page.url.searchParams.toString());
query.set('tag', value);
goto(`?${query.toString()}`, { keepFocus: true, noScroll: true });
selected = value;
};
</script>
<section class="flex justify-center flex-col items-center {className}">
<p class="text-semibold mb-2 md:mb-4">
Sort by category
</p>
<ul class="flex flex-wrap justify-center gap-2">
{#each options as option}
<li>
<Button
class={option === selected
? 'dark:!bg-primary dark:!text-black !bg-black !text-white'
: ''}
variant="cta"
size="medium"
on:click={() => clickHandler(option)}
>
{option}
</Button>
</li>
{/each}
</ul>
</section>

View File

@ -0,0 +1,58 @@
<script lang="ts">
import { formatDate } from '$src/content/utils.js';
export let baseUrl: string;
export let imagesDirectoryName: string;
export let date: string = '';
export let author: string = '';
export let slug: string = '';
export let title: string;
export let image: string;
export let teaserImage: string;
export let excerpt: string;
export let tags: string[] = [];
</script>
<article>
<div class="flex justify-center mt-4 mb-8">
<div class="w-full lg:w-[50rem] leading-[177.7%]">
<img
src="/images/{imagesDirectoryName}/{slug}/{teaserImage || image}"
alt={`${title}`}
class="max-h-[540px] rounded-tl-2xl rounded-tr-[1.3rem]"
/>
<div
class="content-blog prose prose-img:rounded-tl-2xl prose-img:rounded-tr-[1.3rem] max-w-none text-body"
>
<p
class="{tags && tags.length > 0
? '!mb-2'
: '!mb-2'} mt-[1.875rem] text-body text-white"
>
{formatDate(date)}
</p>
{#if tags && tags.length > 0}
<div class="flex mb-2 items-center gap-2">
{#each tags as tag}
<a
data-sveltekit-preload-data="hover"
href="/blog?{new URLSearchParams({tag}).toString()}"
>
<span class="chip variant-ghost-surface text-white">{tag}</span>
</a>
{/each}
</div>
{/if}
<slot />
</div>
</div>
</div>
</article>
<style lang="postcss">
.prose :global(nav.toc) {
@apply hidden;
}
</style>

View File

@ -0,0 +1,82 @@
<script lang="ts">
import { isAnExternalLink } from '$lib/utils/helpers';
import type { BlogPost } from '$lib/types/blog';
export let post: BlogPost;
export let isMostRecent: boolean = false;
export let type: 'blog';
export let layout: 'row' | 'column' = 'column';
export let published: boolean = true;
export let headlineOrder: 'h3' | '' = '';
export let badge: string = '';
export let textWidth: string = '';
const generateURL = (href?: string, slug?: string) => {
if (href) return href;
return `/${type}/${slug}`;
};
$: href = generateURL(post['href'], post.slug);
$: target = post && post['href'] && isAnExternalLink(post['href']) ? '_blank' : undefined;
</script>
<a
{href}
{target}
data-sveltekit-preload-data="hover"
class="card"
data-analytics={`{"context":"grid","variant":"preview"}`}
>
<div class="w-full text-token grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Detailed -->
<a
class="card bg-gradient-to-br variant-gradient-tertiary-primary card-hover overflow-hidden"
href="/blog/{post.slug}"
>
<header>
<img
src="/images/blog/{post.slug}/{post.image}"
class="bg-black/50 w-full aspect-[21/9]"
alt="Post"
/>
</header>
<div class="p-4 space-y-4">
<h6 class="text-2" data-toc-ignore>
<div class="items-center flex gap-2">
{#if post.tags}
{#each post.tags as tag}
<span class="chip variant-ghost-surface">{tag}</span>
{/each}
{/if}
</div>
</h6>
<h3 class="h3" data-toc-ignore>{post.title}</h3>
<article>
<p>
<!-- cspell:disable -->
{post.excerpt}
<!-- cspell:enable -->
</p>
</article>
</div>
<hr class="opacity-50" />
<footer class="p-4 flex justify-start items-center space-x-4">
<div class="flex-auto flex justify-between items-center">
<h6 class="font-bold" data-toc-ignore>By Matt</h6>
<small>
{#if post.date}
<span class="date text-p-small ml-macro">
{new Date(Date.parse(post.date)).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric'
})}
</span>
{/if}
</small>
</div>
</footer>
</a>
</div>
</a>

View File

@ -0,0 +1,4 @@
<script lang="ts">
</script>

View File

@ -1,16 +0,0 @@
export interface MarkdownHeading {
title: string;
slug: string;
level: number;
children: MarkdownHeading[];
}
export interface MarkdownMetadata {
headings: MarkdownHeading[];
}
export interface MdsvexImport<T extends MarkdownMetadata = MarkdownMetadata> {
// Technically not correct but needed to make language-tools happy
default: ConstructorOfATypedSvelteComponent;
metadata: T;
}

View File

@ -0,0 +1,7 @@
import { listBlogPosts } from '$content/blog';
export const load = async () => {
return {
posts: listBlogPosts(),
};
};

View File

@ -0,0 +1,96 @@
<!-- <script lang="ts">
import { formatDate } from '$src/content/utils.js';
import { BlogPost } from '$lib/components/blog';
import * as config from '$lib/config';
export let data;
</script>
<svelte:head>
<title>{config.title}</title>
</svelte:head>
<section>
<ul class="posts">
{#each data.posts as post}
<li class="post">
<a href={post.slug} class="title"></a>
<p class="date">{formatDate(post.date, 'full', 'en')}</p>
<p class="description"></p>
</li>
{/each}
</ul>
<div class="flex w-3/4">
<BlogPost />
<div>Some shit</div>
</div>
</section>
<section class="flex max-w-[600px]">
{#each data.posts as post}
<li class="posts">
<a href={post.slug}>
<BlogPost>
<svelte:fragment slot="header">{post.title}</svelte:fragment>
<p class="date" />
<svelte:fragment slot="footer"
>{formatDate(post.date)}, {post.description}</svelte:fragment
>
</BlogPost>
</a>
</li>
{/each}
</section> -->
<script lang="ts">
import PostPreview from '$lib/components/blog/post-preview.svelte';
import type { PageData } from './$types';
import type { BlogTag } from '$lib/types/blog';
import { page } from '$app/stores';
import { onMount } from 'svelte';
export let data: PageData;
let filter: BlogTag | null = null;
$: posts = data.posts.filter((post) => (filter ? post.tags?.includes(filter) : true));
onMount(() => {
const tagParam = $page.url.searchParams.get('tag');
if (!filter && typeof tagParam == 'string') {
filter = tagParam as BlogTag;
}
});
const displayAmount = 12;
</script>
<div class="blog-layout">
<div>
<div
class="grid m-auto max-w-7xl w-full gap-6 grid-cols-none justify-center md:grid-cols-2 lg:grid-cols-3"
>
{#each posts.slice(0, displayAmount) as post}
<div class="flex justify-center min-w-[20rem] max-w-sm">
<PostPreview {post} type="blog" isMostRecent />
</div>
{/each}
</div>
</div>
{#if posts.slice(displayAmount).length > 0}
<div>
<h2 class="mb-small text-center">Previous posts</h2>
<div
class="previous grid m-auto max-w-7xl w-full gap-6 grid-cols-none justify-center md:grid-cols-2 lg:grid-cols-3"
>
{#each posts.slice(displayAmount) as post}
<div class="flex justify-center min-w-[20rem] max-w-sm">
<PostPreview {post} type="blog" />
</div>
{/each}
</div>
</div>
{/if}
</div>

View File

@ -0,0 +1,27 @@
import { listBlogPosts } from '$content/blog';
import { error } from '@sveltejs/kit';
function shuffle<T>(array: T[]) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
export async function load({ params }) {
const posts = listBlogPosts();
const currentPost = posts.find((post) => post.slug == params.slug);
if (!currentPost) {
throw error(404, `Unable to find blog post "${params.slug}"`);
}
shuffle(posts);
return {
featuredPosts: posts
.filter((post) => post.slug != params.slug)
.filter((p) => p.tags?.some((t) => currentPost.tags?.includes(t)))
.slice(0, 3),
};
}

View File

@ -0,0 +1 @@
<slot />

View File

@ -0,0 +1,14 @@
import { getBlogPost, listBlogPosts } from '$content/blog.js';
import type { PageLoad } from './$types';
export const entries = async () => {
const posts = await listBlogPosts();
return posts
.filter((post) => post.slug !== undefined)
.map((post) => ({ slug: post.slug as string }));
};
export const load: PageLoad = async ({ params, parent }) => {
await parent();
return await getBlogPost(params.slug);
};

View File

@ -0,0 +1,66 @@
import { removeTrailingSlash } from '$lib/utils/helpers';
import type { RequestHandler } from './$types';
import { listBlogPosts } from '$content/blog';
// prettier-ignore
const sitemap = (pages: string[]) => `<?xml version="1.0" encoding="UTF-8" ?>
<urlset
xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:news="https://www.google.com/schemas/sitemap-news/0.9"
xmlns:xhtml="https://www.w3.org/1999/xhtml"
xmlns:mobile="https://www.google.com/schemas/sitemap-mobile/1.0"
xmlns:image="https://www.google.com/schemas/sitemap-image/1.1"
xmlns:video="https://www.google.com/schemas/sitemap-video/1.1"
>
${pages.map((page) => `<url><loc>${removeTrailingSlash(page)}</loc></url>`).join('')}
</urlset>
`;
export const GET: RequestHandler = async () => {
const staticPages = Object.keys(
// For other static pages. Except content pages - changelogs, guides, blog posts, guides etc.
import.meta.glob('/src/routes/**/!(_)*.{svelte,md}'),
)
.filter((page) => {
const filters = [
'/src/routes/index.svelte',
'_',
'404',
'slug]',
'title]',
'src/routes/docs/introduction/getting-started',
'extension-activation',
'unsubscribe',
'subscribe',
'stay-connected',
'extension-uninstall',
'+error',
'+layout',
];
return !filters.find((filter) => page.includes(filter));
})
.map((page) => {
return page
.replace('/src/routes', 'https://www.gitpod.io')
.replace('/index.md', '/')
.replace('.md', '/')
.replace('/index.svelte', '/')
.replace('.svelte', '/')
.replace('/+page', '');
});
const blogPosts = listBlogPosts().map(
(post) => `https://www.gitpod.io/blog/${post.slug}`,
);
const renderedSitemap = sitemap([
...staticPages,
...blogPosts,
]);
return new Response(renderedSitemap, {
headers: {
'Cache-Control': 'max-age=0, s-maxage=3600',
'Content-Type': 'application/xml',
},
});
};