Mdsvex Posts, posts Api. config

This commit is contained in:
matthieu42morin 2023-11-08 23:56:48 +01:00
parent 4fa47b2ac3
commit e4dc0e56cc
15 changed files with 1038 additions and 583 deletions

5
.gitignore vendored
View File

@ -57,4 +57,7 @@ public/api/vendor
/test-results/ /test-results/
/playwright-report/ /playwright-report/
/playwright/.cache/ /playwright/.cache/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

75
mdsvex.config.js Normal file
View File

@ -0,0 +1,75 @@
import { defineMDSveXConfig as defineConfig } from 'mdsvex';
import headings from 'remark-autolink-headings';
import remarkExternalLinks from 'remark-external-links';
import slug from 'remark-slug';
import remarkSetImagePath from './src/lib/utils/remark-set-image-path.js';
import remarkLinkWithImageAsOnlyChild from './src/lib/utils/remark-link-with-image-as-only-child.js';
import remarkHeadingsPermaLinks from './src/lib/utils/remark-headings-permalinks.js';
import { toString } from 'mdast-util-to-string';
import rehypeWrap from 'rehype-wrap-all';
import rehypeImgSize from 'rehype-img-size';
import { h } from 'hastscript';
import { visit } from 'unist-util-visit';
import remarkToc from 'remark-toc';
import getHeadings from './src/lib/utils/get-headings.js';
// import { highlightCode } from './src/lib/utils/highlighter.js';
/** @type {import('mdsvex').MdsvexOptions} */
const config = defineConfig({
extensions: ['.svelte.md', '.md', '.svx'],
smartypants: {
dashes: 'oldschool'
},
// Wait for skeleton to implement Prismjs, for now use <CodeBlock /> in .md files
// highlight: {},
// layout: {
// blog: './src/lib/components/blog/_blog-layout.svelte',
// project: './src/lib/components/projects/_project-layout.svelte',
// _: './src/lib/components/fallback/_layout.svelte'
// },
rehypePlugins: [
[rehypeWrap, { selector: 'table', wrapper: 'div.overflow-auto' }],
[rehypeImgSize, { dir: './static' }],
[
/** Custom rehype plugin to add loading="lazy" to all images */
() => {
return (tree) => {
visit(tree, 'element', (node) => {
if (node.tagName === 'img') {
node.properties.loading = 'lazy';
}
});
};
}
]
],
remarkPlugins: [
[remarkToc, { maxDepth: 3, tight: true }][
(remarkExternalLinks,
{
target: '_blank'
})
],
slug,
[
headings,
{
behavior: 'append',
linkProperties: {},
content: function (node) {
return [
h('span.icon.icon-link header-anchor', {
ariaLabel: toString(node) + ' permalink'
})
];
}
}
],
remarkSetImagePath,
remarkLinkWithImageAsOnlyChild,
remarkHeadingsPermaLinks,
getHeadings
]
});
export default config;

View File

@ -20,57 +20,61 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest", "test": "vitest",
"lint": "prettier --plugin-search-dir . --check . && eslint .", "lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ." "format": "prettier --plugin-search-dir . --write .",
"test-ct": "playwright test -c playwright-ct.config.ts"
}, },
"devDependencies": { "devDependencies": {
"@playwright/experimental-ct-svelte": "^1.39.0",
"@skeletonlabs/skeleton": "2.0.0", "@skeletonlabs/skeleton": "2.0.0",
"@skeletonlabs/tw-plugin": "0.1.0", "@skeletonlabs/tw-plugin": "0.1.0",
"@sveltejs/adapter-cloudflare": "^2.3.3", "@sveltejs/adapter-cloudflare": "^2.3.3",
"@sveltejs/kit": "^1.20.4", "@sveltejs/kit": "^1.27.3",
"@tailwindcss/forms": "0.5.6", "@tailwindcss/forms": "0.5.6",
"@tailwindcss/typography": "0.5.9", "@tailwindcss/typography": "0.5.9",
"@types/js-cookie": "^3.0.5", "@types/js-cookie": "^3.0.5",
"@types/node": "20.5.7", "@types/node": "20.5.7",
"@typescript-eslint/eslint-plugin": "^5.45.0", "@types/prismjs": "^1.26.2",
"@typescript-eslint/parser": "^5.45.0", "@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"autoprefixer": "10.4.15", "autoprefixer": "10.4.15",
"emoji-regex": "^10.3.0", "emoji-regex": "^10.3.0",
"eslint": "^8.28.0", "eslint": "^8.53.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.10.0",
"eslint-plugin-svelte": "^2.30.0", "eslint-plugin-svelte": "^2.34.1",
"js-cookie": "^3.0.5",
"postcss": "8.4.29",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.10.1",
"sass": "^1.66.1",
"svelte": "^4.0.5",
"svelte-check": "^3.4.3",
"tailwindcss": "3.3.3",
"tslib": "^2.4.1",
"typescript": "^5.2.2",
"vite": "^4.4.2",
"vite-plugin-tailwind-purgecss": "0.1.3",
"vitest": "^0.34.0"
},
"dependencies": {
"@floating-ui/dom": "1.5.1",
"@threlte/core": "^6.0.10",
"@threlte/extras": "^7.0.0",
"@yushijinhun/three-minifier-rollup": "^0.4.0",
"hastscript": "^8.0.0", "hastscript": "^8.0.0",
"highlight.js": "11.8.0", "js-cookie": "^3.0.5",
"linkedom": "^0.15.3",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"mdsvex": "^0.11.0", "mdsvex": "^0.11.0",
"prismjs": "^1.29.0", "postcss": "8.4.29",
"prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.1",
"rehype-img-size": "^1.0.1", "rehype-img-size": "^1.0.1",
"rehype-wrap-all": "^1.1.0", "rehype-wrap-all": "^1.1.0",
"remark-autolink-headings": "^7.0.1", "remark-autolink-headings": "^7.0.1",
"remark-external-links": "^9.0.1", "remark-external-links": "^9.0.1",
"remark-slug": "^7.0.1", "remark-slug": "^7.0.1",
"sass": "^1.69.5",
"svelte": "^4.2.2",
"svelte-check": "^3.5.2",
"tailwindcss": "3.3.3",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"unist-util-visit": "^5.0.0",
"vite": "^4.5.0",
"vite-plugin-tailwind-purgecss": "0.1.3",
"vitest": "^0.34.6"
},
"dependencies": {
"@floating-ui/dom": "1.5.1",
"@threlte/core": "^6.1.0",
"@threlte/extras": "^7.5.0",
"@yushijinhun/three-minifier-rollup": "^0.4.0",
"highlight.js": "11.8.0",
"linkedom": "^0.15.6",
"prismjs": "^1.29.0",
"remark-toc": "^9.0.0",
"rss": "^1.2.2", "rss": "^1.2.2",
"svelte-preprocess": "^5.0.4", "svelte-preprocess": "^5.0.4"
"unist-util-visit": "^5.0.0"
}, },
"type": "module" "type": "module"
} }

46
playwright-ct.config.ts Normal file
View File

@ -0,0 +1,46 @@
import { defineConfig, devices } from '@playwright/experimental-ct-svelte';
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './',
/* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */
snapshotDir: './__snapshots__',
/* Maximum time one test can run for. */
timeout: 10 * 1000,
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Port to use for Playwright component endpoint. */
ctPort: 3100,
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
<script lang="ts">
import { formatDate } from '$src/content/utils';
import '$lib/assets/prism-nord.css';
export let baseUrl: string;
export let imagesDirectoryName: string;
export let date: string = '';
export let slug: string = '';
export let title: string;
export let image: string;
export let teaserImage: 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="prose prose-img:rounded-tl-2xl prose-img:rounded-tr-[1.3rem] max-w-none text-base"
>
<p class="{tags && tags.length > 0 ? '!mb-2' : '!mb-2'} mt-8 text-base">
{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">{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

@ -1,15 +1,16 @@
import type { MarkdownMetadata } from '$lib/contents/types'; import type { MarkdownMetadata } from '$lib/contents/types';
export type BlogTag = 'Projects' | 'Ideas' | 'Updates' | ''; export type BlogTag = 'Projects' | 'Blog' | 'Updates' | '';
export interface BlogPost extends MarkdownMetadata { export interface BlogPost extends MarkdownMetadata {
title: string;
subtitle?: string;
tags?: BlogTag[];
modified?: string;
date?: string; date?: string;
excerpt: string; excerpt: string;
image: string; image: string;
slug?: string; slug?: string;
href?: string; href?: string;
tags?: BlogTag[];
subtitle?: string;
title: string;
published: boolean; published: boolean;
} }

View File

@ -0,0 +1,70 @@
/**
* Credit goes to @Xananax for providing this solution within the gist https://gist.github.com/Xananax/5dca3a1dd7070e4fdebe2927e4aeb55b
*/
import { join, basename, extname } from 'path';
export const defaults = {
extensions: ['.svelte.md', '.md', '.svx'],
dir: `$lib`,
list: []
};
/**
* Injects global imports in all your mdsvex files
* Specify:
* - the root dir (defaults to `src/lib`)
* - the array list of components (with extension), like `['Component.svelte']`
* - the valid extensions list as an array (defaults to `['.svelte.md', '.md', '.svx']`)
*
* If you want the component name to be different from the file name, you can specify an array
* of arrays: `['Component.svelte', ['Another', 'AnotherComp.svelte'], 'ThirdComp.svelte']`
*
* @param {Object} options options described above
* @returns a preprocessor suitable to plug into the `preprocess` key of the svelte config
*/
export const mdsvexGlobalComponents = (options = {}) => {
const { extensions, dir, list } = { ...defaults, ...options };
const extensionsRegex = new RegExp(
'(' + extensions.join('|').replace(/\./g, '\\.') + ')$',
'i'
);
if (!list || !list.length || !Array.isArray(list)) {
throw new Error(`"list" option must be an array and contain at least one element`);
}
const imports = list
.map((entry) => {
let name = '';
if (Array.isArray(entry)) {
name = entry[0];
entry = entry[1];
}
const ext = extname(entry);
const path = join(dir, entry);
name = name || basename(entry, ext);
return `\nimport ${name} from "${path}";`;
})
.join('\n');
const preprocessor = {
script(thing) {
const { content, filename, attributes, markup } = thing;
if (!filename.match(extensionsRegex)) {
return { code: content };
}
const hasModuleContext = /^<script context="module">/.test(markup);
const isModulePass = attributes?.context === 'module';
if (!isModulePass || !hasModuleContext) {
return { code: content };
}
const isValidPass = (hasModuleContext && isModulePass) || !hasModuleContext;
if (!isValidPass) {
return { code: content };
}
return { code: `${imports}\n${content}` };
}
};
return preprocessor;
};

View File

@ -3,22 +3,15 @@ import { visit } from 'unist-util-visit';
const imagesRelativeUrlPattern = '/images/'; const imagesRelativeUrlPattern = '/images/';
const visitor = (node) => { const visitor = (node) => {
if ( if (node.type === 'image' && node.url.indexOf(imagesRelativeUrlPattern) > 0) {
node.type === 'image' && node.url = node.url.substring(node.url.indexOf(imagesRelativeUrlPattern) + ''.length);
node.url.indexOf(imagesRelativeUrlPattern) > 0
) {
node.url = node.url.substring(
node.url.indexOf(imagesRelativeUrlPattern) + ''.length,
);
} }
}; };
export default () => async (tree, vFile) => { export default () => async (tree, vFile) => {
if ( if (
vFile.filename.indexOf('src/routes/docs/') > 0 ||
vFile.filename.indexOf('src/routes/blog/') > 0 || vFile.filename.indexOf('src/routes/blog/') > 0 ||
vFile.filename.indexOf('src/routes/guides/') > 0 || vFile.filename.indexOf('src/routes/projects/') > 0
vFile.filename.indexOf('src/routes/customers/') > 0
) { ) {
visit(tree, visitor); visit(tree, visitor);
} }

26
src/routes/+error.svelte Normal file
View File

@ -0,0 +1,26 @@
<!-- This page handles any error encountered by the site. -->
<script>
import { page } from '$app/stores';
</script>
{#if $page.status === 404}
<div class="flex flex-col items-center">
<div
class="relative mb-[3.33vh] flex h-96 w-96 items-center justify-center rounded-full bg-404"
>
<h1 class="h1 absolute text-h1 leading-[5rem]">404</h1>
</div>
<h2 class="h4 mb-x-small">You just hit a route that doesn't exist</h2>
</div>
{:else}
<h2>{$page.status}</h2>
{#if $page.error}
<p class="subhead">{$page.error.message}</p>
{/if}
<p><strong>Sorry!</strong> Maybe try one of these links?</p>
<ul>
<li><a href="/">Home</a></li>
</ul>
{/if}

View File

@ -3,6 +3,7 @@
import { AppShell } from '@skeletonlabs/skeleton'; import { AppShell } from '@skeletonlabs/skeleton';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import MainFooter from '$lib/components/MainFooter.svelte'; import MainFooter from '$lib/components/MainFooter.svelte';
import MainHeader from '$lib/components/MainHeader.svelte';
// Floating UI for Popups // Floating UI for Popups
import { computePosition, autoUpdate, flip, shift, offset, arrow } from '@floating-ui/dom'; import { computePosition, autoUpdate, flip, shift, offset, arrow } from '@floating-ui/dom';
@ -65,9 +66,8 @@
// Highlight JS // Highlight JS
import hljs from 'highlight.js'; import hljs from 'highlight.js';
import 'highlight.js/styles/github-dark.css'; import 'highlight.js/styles/nord.css';
import { storeHighlightJs } from '@skeletonlabs/skeleton'; import { storeHighlightJs } from '@skeletonlabs/skeleton';
import MainHeader from '$lib/components/MainHeader.svelte';
storeHighlightJs.set(hljs); storeHighlightJs.set(hljs);
</script> </script>

2
src/routes/+layout.ts Normal file
View File

@ -0,0 +1,2 @@
export const prerender = true;
export const trailingSlash = 'never';

View File

@ -43,7 +43,7 @@
</section> --> </section> -->
<script lang="ts"> <script lang="ts">
import PostPreview from '$lib/components/blog/post-preview.svelte'; import PostPreview from '$lib/components/blog/BlogLayout.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import type { BlogTag } from '$lib/types/blog'; import type { BlogTag } from '$lib/types/blog';
import { page } from '$app/stores'; import { page } from '$app/stores';

View File

@ -0,0 +1,16 @@
<script lang="ts">
import BlogContentLayout from '$lib/components/blog/BlogLayout.svelte';
import type { PageData } from './$types';
export let data: PageData;
</script>
<!-- <svelte:head>
<title>{data.post.title}</title>
<meta property="og:type" content="article" />
<meta property="og:title" content={data.post.title} />
</svelte:head> -->
<BlogContentLayout post={data.post}>
<svelte:component this={data.Component} />
</BlogContentLayout>