46
loading...
This website collects cookies to deliver better user experience
<h1>My heading</h2>
, while in Markdown you write # My heading
. There are similar shorter equivalents for writing lists, adding links, pictures and so on. All in all it means you spend less time tracking a missing close tag and concentrate on getting your thoughts down. On top MDsveX makes customising blog posts a lot easier.git clone https://github.com/rodneylab/sveltekit-blog-mdx.git
cd sveltekit-blog-mdx
npm install
npm run dev
pnpm install
instead of npm install
if you have pnpm set up..
├── README.md
├── jsconfig.json
├── netlify.toml
├── package.json
├── pnpm-lock.yaml
├── src
│ ├── app.html
│ ├── hooks.js
│ ├── lib
│ │ ├── components
│ │ │ ├── BannerImage.svelte
│ │ │ ├── ...
│ │ │ └── SEO
│ │ ├── config
│ │ │ └── website.js
│ │ ├── constants
│ │ │ └── entities.js
│ │ ├── styles
│ │ │ ├── index.scss
│ │ │ ├── normalise.css
│ │ │ ├── styles.scss
│ │ │ └── variables.scss
│ │ └── utilities
│ │ └── blog.js
│ └── routes
│ ├── [slug].json.js
│ ├── __layout.svelte
│ ├── best-medium-format-camera-for-starting-out
│ │ └── index.md
│ ├── contact.svelte
│ ├── folding-camera
│ │ └── index.md
│ ├── index.json.js
│ ├── index.svelte
│ └── twin-lens-reflex-camera
│ └── index.md
├── static
│ ├── favicon.ico
│ └── robots.txt
└── svelte.config.js
hooks.js
we define Content Security Policy (CSP) and other HTTP security headers in here. More on this later.src/lib/components
these are the components we use in pages.src/lib/config/website.js
for convenience we define properties for the site here such as the site title, contact email addresses and social media accounts. Some properties feed from environment variables. See the earlier post on getting started with SvelteKit for more on environment variables in SvelteKit.
src/lib/styles
does what you expect! We use SCSS for styling and source self-hosted fonts in the layouts (we'll see this further down the post).
src/utilities/blog.js
this file contains some code for helping us transform the markdown in blog posts to Svelte. As well as that they help extract fields in the frontmatter (this is the metadata we include at the top fo the blog post index.md
files).src/routes/[slug].json.js
this is essentially a template for blog post data. One of these file is generated at build for each blog post. It is used to extract data needed in the Svelte file used to generate the post's HTML.
__layout.svelte
this is a generalised layout template used both for main site pages and individual blog posts. Blog post data it loaded from this layout.
src/routes/best-medium-format-camera-for-starting-out
this is a folder for a blog post. The blog post slug is taken from the folder name, meaning this folder creates a blog post at www.example.com/best-medium-format-camera-for-starting-out
. The actual Svelte in Markdown content for the post is found in the index.md file. Create more blog posts by creating new folders with the same structure.
BlogRoll
component.--------
postTitle: 'Best Medium Format Camera for Starting out'
focusKeyphrase: 'best medium format camera'
datePublished: '2021-04-07T16:04:42.000+0100'
lastUpdated: '2021-04-14T10:17:52.000+0100'
seoMetaDescription: "Best medium format camera for starting out is probably a question at the front of your mind right now! Let's take a look."
featuredImage: 'best-medium-format-camera-for-starting-out.jpg'
featuredImageAlt: 'Photograph of a Hasselblad medium format camera with the focusing screen exposed'
ogImage: ''
ogSquareImage: ''
twitterImage: ''
categories: ''
tags: ''
--------
## What is a Medium Format Camera?
If you are old enough to remember the analogue film camera era, chances are it is the 35 mm canisters with the track cut down the side that first come to mind. Shots normally had a 3:2 aspect ratio measuring 36×24 mm.
onClick
event handlers in the Gatsby (React) version. In the Svelte version we on:mouseenter
, on:mouseleave
and on:mousedown
inline handlers....
const handleMouseEnter = (event) => {
event.target.style.cursor = 'pointer';
};
const handleMouseLeave = (event) => {
event.target.style.cursor = 'default';
};
const handleMouseDown = async () => {
goto(\`/\${slug}/\`);
};
const date = dayjs(datePublished);
const dateString = \`\${date.format('D')} \${date.format('MMM')}\`;
const idString = \`blog-post-summary-\${slug}\`;
</script>
<div
class="container"
on:mouseenter={handleMouseEnter}
on:mouseleave={handleMouseLeave}
on:mousedown={handleMouseDown}
>
<div class="content">
<h3>
<a aria-label={\`Open \${postTitle} blog post\`} aria-describedby={idString} href={\`/\${slug}/\`}
>{postTitle}</a
>
</h3>
<p>{dateString}</p>
<p>{seoMetaDescription}</p>
<span id={idString} aria-hidden="true">Read more {H_ELLIPSIS_ENTITY}</span>
</div>
</div>
const BlogPostSummary = ({
frontmatter: { datePublished, postTitle, seoMetaDescription },
slug,
}) => {
const containerNode = useRef();
const titleNode = useRef();
useEffect(() => {
if (containerNode.current) {
// deliberately set style with javascript and not CSS for accessibility reasons
containerNode.current.style.cursor = 'pointer';
}
const listener = (event) => {
if (containerNode.current && !titleNode.current.contains(event.target)) {
navigate(\`/\${slug}\`);
}
};
containerNode.current.addEventListener('mousedown', listener);
return () => {
if (containerNode.current) {
containerNode.current.removeEventListener('mousedown', listener);
}
};
}, [containerNode, titleNode]);
const date = dayjs(datePublished);
const idString = \`blog-post-summary-\${slug.slice(0, -1)}\`;
return (
<div className={container} ref={containerNode}>
<div className={content}>
<h3 ref={titleNode}>
<Link
aria-label={`Open ${postTitle} blog post`}
aria-describedby={idString}
to={`/${slug}`}
>
{postTitle}
</Link>
</h3>
<p>{`${date.format('D')} \${date.format('MMM')}`}</p>
<p>{seoMetaDescription}</p>
<span aria-hidden id={idString}>
Read more {H_ELLIPSIS_ENTITY}
</span>
</div>
</div>
);
};
<script context="module">
/**
* @type {import('@sveltejs/kit').Load}
*/
export const prerender = true;
...
@fontsource
npm package for the font we want to use on our site and import this in the gatsby-browser.js
file to make it accessible throughout the site. Self hosting makes the page load faster, saving the user's browser having to connect to a different origin to download the fonts it needs. In SvelteKit, it's not too different. Once again, we install the font packages, we just include them differently. In SvelteKt, we can add them to the default layout file if they are used throughout the site:<script>
// Lora - supported variants:
// weights: [400, 500, 600, 700]
// styles: italic, normal
import '@fontsource/lora/400.css';
import '@fontsource/lora/600.css';
import '@fontsource/lora/700.css';
import '@fontsource/lora/700-italic.css';
...
</script>
sass
and Svelte preprocessor packages:
npm i -D sass svelte-preprocess
/** @type {import('@sveltejs/kit').Config} */
import adapter from '@sveltejs/adapter-netlify';
import preprocess from 'svelte-preprocess';
const config = {
preprocess: preprocess({
scss: {
prependData: "@import 'src/lib/styles/styles.scss';"
}
}),
...
8
can be used to include any variables which you want to expose to every style element.Define any global styles in the files in src/lib/styles
directory.
Import styles where components or pages need them:
<script>
...
import '$lib/styles/normalise.css';
import '$lib/styles/index.scss';
...
</script>
scss
as the language:
<style lang="scss">
.container {
display: flex;
flex-wrap: wrap;
flex-direction: column;
justify-content: space-between;
padding: $spacing-4 $spacing-0 $spacing-0;
margin: $spacing-0 auto;
min-height: 100vh;
}
...
src/hooks.js
file.Content-Security-Policy-Report-Only
to Content-Security-Policy
. Remember to comment out the report only line when you do this. hooks.js
file is in the default location, so you should not have to include it in svelte.config.js
. The Climate SvelteKit Blog Starter SvelteKit config, includes it just for completeness though. You will almost certainly need to customise the CSP HTTP headers in the hooks file for you application.// https://gist.github.com/acoyfellow/d8e86979c66ebea25e1643594e38be73
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
// https://scotthelme.co.uk/content-security-policy-an-introduction/
// scanner: https://securityheaders.com/
const rootDomain = import.meta.env.VITE_DOMAIN; // or your server IP for dev
const directives = {
'base-uri': ["'self'"],
'child-src': ["'self'"],
'connect-src': ["'self'", 'ws://localhost:*'],
'img-src': ["'self'", 'data:', import.meta.env.VITE_IMGIX_DOMAIN],
'font-src': ["'self'", 'data:'],
'form-action': ["'self'"],
'frame-ancestors': ["'self'"],
'frame-src': ["'self'"],
'manifest-src': ["'self'"],
'media-src': ["'self'", 'data:'],
'object-src': ["'none'"],
'style-src': ["'self'", "'unsafe-inline'"],
'default-src': ["'self'", rootDomain, \`ws://\${rootDomain}\`],
'script-src': ["'self'", "'unsafe-inline'"],
'worker-src': ["'self'"],
'report-to': ["'csp-endpoint'"],
'report-uri': [
\`https://sentry.io/api/\${import.meta.env.VITE_SENTRY_PROJECT_ID}/security/?sentry_key=\${
import.meta.env.VITE_SENTRY_KEY
}\`
]
};
const csp = Object.entries(directives)
.map(([key, arr]) => key + ' ' + arr.join(' '))
.join('; ');
export async function handle({ request, resolve }) {
const response = await resolve(request);
console.log('handle', { ...response.headers });
return {
...response,
headers: {
...response.headers,
'X-Frame-Options': 'SAMEORIGIN',
'Referrer-Policy': 'no-referrer',
'Permissions-Policy':
'accelerometer=(), autoplay=(), camera=(), document-domain=(), encrypted-media=(), fullscreen=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), geolocation=()',
'X-Content-Type-Options': 'nosniff',
'Content-Security-Policy-Report-Only': csp,
'Expect-CT': \`max-age=86400, report-uri="https://sentry.io/api/\${
import.meta.env.VITE_SENTRY_PROJECT_ID
}/security/?sentry_key=\${import.meta.env.VITE_SENTRY_KEY}"\`,
'Report-To': \`{group: "csp-endpoint", "max_age": 10886400, "endpoints": [{"url": "https://sentry.io/api/\${
import.meta.env.VITE_SENTRY_PROJECT_ID
}/security/?sentry_key=\${import.meta.env.VITE_SENTRY_KEY}"}]}\`,
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload'
}
};
}
unsafe-inline
. I will look into how to do this when I get a chance! If you have already written a custom script to handle it, I would love to hear from you!.env
file (see .env.EXAMPLE
for a template).BannerImage
component is able to query Imgix for the image url and srcset
to create a responsive image. As a temporary hack, I have manually generated these data so that the entire site can be static (this is related to the Netlify adapter issue mentioned earlier). If you also want to keep your site static, you have a choice of either also generating the data manually (or with a script) or using an alternative method for generating images.