Make blog post index listing

This commit is contained in:
Michal Vanko 2019-11-10 21:37:50 +01:00
parent 839e169f3a
commit 27b93c6e02
13 changed files with 611 additions and 505 deletions

View File

@ -4,8 +4,10 @@ title: Ide to ?
date: 2019-08-09T17:24:13.481Z date: 2019-08-09T17:24:13.481Z
thumbnail: /images/uploads/screenshot.gif thumbnail: /images/uploads/screenshot.gif
rating: 4 rating: 4
tags: ['dev']
--- ---
![Photo presentation of a responzIO device ](/images/uploads/responzio.png "responzIO device")
![Photo presentation of a responzIO device ](/images/uploads/responzio.png 'responzIO device')
# Invenit vaporibus in educat visa Cererem dissimiles # Invenit vaporibus in educat visa Cererem dissimiles

View File

@ -4,7 +4,9 @@ title: Anothert one
date: 2019-11-03T11:01:32.621Z date: 2019-11-03T11:01:32.621Z
thumbnail: /images/uploads/responzio.png thumbnail: /images/uploads/responzio.png
rating: 2 rating: 2
tags: []
--- ---
# Simul rursus animal Scythiae # Simul rursus animal Scythiae
## Latus saltibus et mihi ## Latus saltibus et mihi
@ -30,7 +32,7 @@ penitus quis.
Scis nervo properatus [sitis](http://www.infernas.io/continui-atque): quid, Scis nervo properatus [sitis](http://www.infernas.io/continui-atque): quid,
venientique et felix ficta iactantem Echidnae humanae. Apollineos adspice, ut venientique et felix ficta iactantem Echidnae humanae. Apollineos adspice, ut
est iam numquamque artus sanguis frondes coniugiique vibrantia taceam, ceu. In est iam numquamque artus sanguis frondes coniugiique vibrantia taceam, ceu. In
Hesperio tenet ara honorem *laude* partim deus, Stymphalides pressa quoniam mea Hesperio tenet ara honorem _laude_ partim deus, Stymphalides pressa quoniam mea
saligna vides, cortinaque. Opem inductas et ignes auras oppidaque calcataque saligna vides, cortinaque. Opem inductas et ignes auras oppidaque calcataque
huius hic viribus luna pennis, sub ille altae Rhanisque. huius hic viribus luna pennis, sub ille altae Rhanisque.

View File

@ -1,6 +1,6 @@
<div> <div>
<table <table
style="width: 480px; font-size: 16px; font-family: Cantarell, Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; line-height:normal;" style="width: 480px; font-size: 12px; font-family: Cantarell, Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; line-height:normal;"
cellpadding="0" cellpadding="0"
cellspacing="0" cellspacing="0"
> >
@ -8,29 +8,29 @@
<tr> <tr>
<td <td
rowspan="3" rowspan="3"
style="vertical-align: center; border-right: 2px solid #212138; width: 48px" style="vertical-align: center; border-right: 1px solid #212138; width: 80px"
> >
<img <img
src="https://michalvanko.dev/images/m-logo.png" src="https://michalvanko.dev/images/m-logo.png"
alt="m-logo" alt="m-logo"
title="Logo" title="Logo"
style="height: 60px;" style="height: 46px;"
/> />
</td> </td>
<td <td
style="color: #212138; font-size: 16px; padding-bottom: 4px; padding-left: 8px;" style="color: #212138; font-size: 12px; padding-bottom: 4px; padding-left: 8px;"
> >
<span>Michal Vanko</span> <span>Michal Vanko</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td style="color: 42436a; font-size: 14px; padding-left: 8px;"> <td style="color: 42436a; font-size: 11px; padding-left: 8px;">
<span>Software Architect and Consultant</span> <span>Software Architect and Consultant</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td style="font-size: 14px; padding-left: 8px;"> <td style="font-size: 11px; padding-left: 8px;">
<a <a
target="_blank" target="_blank"
href="https://michalvanko.dev" href="https://michalvanko.dev"

673
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "TODO", "name": "michalvankodev",
"description": "TODO", "description": "My personal website with blog",
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"dev": "sapper dev", "dev": "sapper dev",
@ -12,27 +12,30 @@
"test": "run-p --race dev cy:run" "test": "run-p --race dev cy:run"
}, },
"dependencies": { "dependencies": {
"compression": "^1.7.1", "compression": "^1.7.4",
"date-fns": "^2.7.0",
"front-matter": "^3.0.2", "front-matter": "^3.0.2",
"marked": "^0.7.0", "marked": "^0.7.0",
"polka": "^0.5.0", "parse5": "^5.1.1",
"sirv": "^0.4.0" "polka": "^0.5.2",
"ramda": "^0.26.1",
"sirv": "^0.4.2"
}, },
"devDependencies": { "devDependencies": {
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"sapper": "^0.27.0", "sapper": "^0.27.9",
"svelte": "^3.0.0", "svelte": "^3.12.1",
"@babel/core": "^7.0.0", "@babel/core": "^7.6.4",
"@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.0.0", "@babel/plugin-transform-runtime": "^7.6.2",
"@babel/preset-env": "^7.0.0", "@babel/preset-env": "^7.6.3",
"@babel/runtime": "^7.0.0", "@babel/runtime": "^7.6.3",
"rollup": "^1.12.0", "rollup": "^1.26.3",
"rollup-plugin-babel": "^4.0.2", "rollup-plugin-babel": "^4.3.3",
"rollup-plugin-commonjs": "^10.0.0", "rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-replace": "^2.0.0", "rollup-plugin-replace": "^2.2.0",
"rollup-plugin-svelte": "^5.0.1", "rollup-plugin-svelte": "^5.1.0",
"rollup-plugin-terser": "^4.0.4" "rollup-plugin-terser": "^5.1.2"
} }
} }

View File

@ -0,0 +1,8 @@
import marked from 'marked'
export function parseField(field) {
return item => ({
...item,
[field]: marked(item[field]),
})
}

View File

@ -1,28 +1,30 @@
import posts from './_posts.js'; import { readFile } from 'fs'
import { promisify } from 'util'
import fm from 'front-matter'
import { parseField } from '../../markdown/parse-markdown'
const lookup = new Map(); export async function get(req, res, next) {
posts.forEach(post => { // the `slug` parameter is available because
lookup.set(post.slug, JSON.stringify(post)); // this file is called [slug].json.js
}); const { slug } = req.params
export function get(req, res, next) { let postSource
// the `slug` parameter is available because try {
// this file is called [slug].json.js postSource = await promisify(readFile)(`_posts/blog/${slug}.md`, 'utf-8')
const { slug } = req.params; } catch (e) {
if (e.code === 'ENOENT') {
res.statusCode = 404
res.end('Post not found \n' + e.toString())
return
}
res.statusCode = 500
res.end('Error loading post source file. \n' + e.toString())
return
}
if (lookup.has(slug)) { const parsedPost = fm(postSource)
res.writeHead(200, { const response = parseField('body')(parsedPost)
'Content-Type': 'application/json'
});
res.end(lookup.get(slug)); res.setHeader('Content-Type', 'application/json')
} else { res.end(JSON.stringify(response))
res.writeHead(404, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: `Not found`
}));
}
} }

View File

@ -1,92 +0,0 @@
// Ordinarily, you'd generate this data from markdown files in your
// repo, or fetch them from a database of some kind. But in order to
// avoid unnecessary dependencies in the starter template, and in the
// service of obviousness, we're just going to leave it here.
// This file is called `_posts.js` rather than `posts.js`, because
// we don't want to create an `/blog/posts` route — the leading
// underscore tells Sapper not to do that.
const posts = [
{
title: 'What is Sapper?',
slug: 'what-is-sapper',
html: `
<p>First, you have to know what <a href='https://svelte.dev'>Svelte</a> is. Svelte is a UI framework with a bold new idea: rather than providing a library that you write code with (like React or Vue, for example), it's a compiler that turns your components into highly optimized vanilla JavaScript. If you haven't already read the <a href='https://svelte.dev/blog/frameworks-without-the-framework'>introductory blog post</a>, you should!</p>
<p>Sapper is a Next.js-style framework (<a href='blog/how-is-sapper-different-from-next'>more on that here</a>) built around Svelte. It makes it embarrassingly easy to create extremely high performance web apps. Out of the box, you get:</p>
<ul>
<li>Code-splitting, dynamic imports and hot module replacement, powered by webpack</li>
<li>Server-side rendering (SSR) with client-side hydration</li>
<li>Service worker for offline support, and all the PWA bells and whistles</li>
<li>The nicest development experience you've ever had, or your money back</li>
</ul>
<p>It's implemented as Express middleware. Everything is set up and waiting for you to get started, but you keep complete control over the server, service worker, webpack config and everything else, so it's as flexible as you need it to be.</p>
`
},
{
title: 'How to use Sapper',
slug: 'how-to-use-sapper',
html: `
<h2>Step one</h2>
<p>Create a new project, using <a href='https://github.com/Rich-Harris/degit'>degit</a>:</p>
<pre><code>npx degit "sveltejs/sapper-template#rollup" my-app
cd my-app
npm install # or yarn!
npm run dev
</code></pre>
<h2>Step two</h2>
<p>Go to <a href='http://localhost:3000'>localhost:3000</a>. Open <code>my-app</code> in your editor. Edit the files in the <code>src/routes</code> directory or add new ones.</p>
<h2>Step three</h2>
<p>...</p>
<h2>Step four</h2>
<p>Resist overdone joke formats.</p>
`
},
{
title: 'Why the name?',
slug: 'why-the-name',
html: `
<p>In war, the soldiers who build bridges, repair roads, clear minefields and conduct demolitions all under combat conditions are known as <em>sappers</em>.</p>
<p>For web developers, the stakes are generally lower than those for combat engineers. But we face our own hostile environment: underpowered devices, poor network connections, and the complexity inherent in front-end engineering. Sapper, which is short for <strong>S</strong>velte <strong>app</strong> mak<strong>er</strong>, is your courageous and dutiful ally.</p>
`
},
{
title: 'How is Sapper different from Next.js?',
slug: 'how-is-sapper-different-from-next',
html: `
<p><a href='https://github.com/zeit/next.js'>Next.js</a> is a React framework from <a href='https://zeit.co'>Zeit</a>, and is the inspiration for Sapper. There are a few notable differences, however:</p>
<ul>
<li>It's powered by <a href='https://svelte.dev'>Svelte</a> instead of React, so it's faster and your apps are smaller</li>
<li>Instead of route masking, we encode route parameters in filenames. For example, the page you're looking at right now is <code>src/routes/blog/[slug].html</code></li>
<li>As well as pages (Svelte components, which render on server or client), you can create <em>server routes</em> in your <code>routes</code> directory. These are just <code>.js</code> files that export functions corresponding to HTTP methods, and receive Express <code>request</code> and <code>response</code> objects as arguments. This makes it very easy to, for example, add a JSON API such as the one <a href='blog/how-is-sapper-different-from-next.json'>powering this very page</a></li>
<li>Links are just <code>&lt;a&gt;</code> elements, rather than framework-specific <code>&lt;Link&gt;</code> components. That means, for example, that <a href='blog/how-can-i-get-involved'>this link right here</a>, despite being inside a blob of HTML, works with the router as you'd expect.</li>
</ul>
`
},
{
title: 'How can I get involved?',
slug: 'how-can-i-get-involved',
html: `
<p>We're so glad you asked! Come on over to the <a href='https://github.com/sveltejs/svelte'>Svelte</a> and <a href='https://github.com/sveltejs/sapper'>Sapper</a> repos, and join us in the <a href='https://svelte.dev/chat'>Discord chatroom</a>. Everyone is welcome, especially you!</p>
`
}
];
posts.forEach(post => {
post.html = post.html.replace(/^\t{3}/gm, '');
});
export default posts;

View File

@ -1,16 +1,36 @@
import posts from './_posts.js'; import { readdir, readFile } from 'fs'
import { promisify } from 'util'
import fm from 'front-matter'
import marked from 'marked'
const contents = JSON.stringify(posts.map(post => { export async function get(req, res) {
return { const files = await promisify(readdir)(`_posts/blog/`, 'utf-8')
title: post.title,
slug: post.slug
};
}));
export function get(req, res) { const contents = await Promise.all(
res.writeHead(200, { files.map(async file => {
'Content-Type': 'application/json' const fileContent = await promisify(readFile)(
}); `_posts/blog/${file}`,
'utf-8'
)
const parsedAttributes = fm(fileContent)
res.end(contents); const lineOfTextRegExp = /^(?:\w|\[).+/gm
} const lines = parsedAttributes.body
.match(lineOfTextRegExp)
.slice(0, 4)
.join('\n')
const preview = marked(lines)
return {
...parsedAttributes.attributes,
preview,
}
})
)
res.writeHead(200, {
'Content-Type': 'application/json',
})
res.end(JSON.stringify(contents))
}

View File

@ -1,34 +1,103 @@
<script context="module"> <script context="module">
export function preload({ params, query }) { export function preload({ params, query }) {
return this.fetch(`blog.json`).then(r => r.json()).then(posts => { return this.fetch(`blog.json`)
return { posts }; .then(r => r.json())
}); .then(posts => {
} return { posts }
})
}
</script> </script>
<script> <script>
export let posts; import { format } from 'date-fns'
export let posts
</script> </script>
<style> <style>
ul { .post-list {
margin: 0 0 1em 0; margin: 0;
line-height: 1.5; padding: 0;
} line-height: 1.5;
list-style: none;
}
.post-list > li:not(:last-child) {
margin-bottom: 2em;
}
.tags-list {
list-style: none;
margin: 0;
padding: 0;
display: inline;
}
.tags-list li {
display: inline;
font-style: italic;
}
time {
font-style: italic;
}
footer {
display: flex;
font-size: 0.85em;
justify-content: space-between;
padding-top: 0.2em;
margin-top: 0.4em;
border-top: 1px solid #c0c1e1;
}
.lighten {
color: #595a8f;
}
</style> </style>
<svelte:head> <svelte:head>
<title>Blog</title> <title>My blog @michalvankodev</title>
</svelte:head> </svelte:head>
<h1>Recent posts</h1> <h1>Recent posts</h1>
<ul> <ul class="post-list">
{#each posts as post} {#each posts as post}
<!-- we're using the non-standard `rel=prefetch` attribute to <!-- we're using the non-standard `rel=prefetch` attribute to
tell Sapper to load the data for the page as soon as tell Sapper to load the data for the page as soon as
the user hovers over the link or taps it, instead of the user hovers over the link or taps it, instead of
waiting for the 'click' event --> waiting for the 'click' event -->
<li><a rel='prefetch' href='blog/{post.slug}'>{post.title}</a></li> <li>
{/each} <article>
</ul> <header>
<h2>
<a rel="prefetch" href="blog/{post.slug}">{post.title}</a>
</h2>
</header>
{@html post.preview}
<footer>
<div class="article-tags">
{#if post.tags.length > 0}
<span class="lighten">Tags:</span>
<ul class="tags-list">
{#each post.tags as tag}
<li>
<a href="blog?tag={tag}">{tag}</a>
</li>
{/each}
</ul>
{/if}
</div>
<div class="created-at">
<span class="lighten">Published on</span>
<time datetime={post.date}>
{format(new Date(post.date), "do MMMM',' y")}
</time>
</div>
</footer>
</article>
</li>
{/each}
</ul>

View File

@ -2,6 +2,7 @@ import { readFile } from 'fs'
import { promisify } from 'util' import { promisify } from 'util'
import fm from 'front-matter' import fm from 'front-matter'
import marked from 'marked' import marked from 'marked'
import { parseField } from '../../markdown/parse-markdown'
export async function get(req, res, next) { export async function get(req, res, next) {
let pageSource let pageSource
@ -14,14 +15,16 @@ export async function get(req, res, next) {
} }
const parsed = fm(pageSource) const parsed = fm(pageSource)
const workHistory = (parsed.attributes.work_history || []).map(parseField('description')) const workHistory = (parsed.attributes.work_history || []).map(
parseField('description')
)
const projects = (parsed.attributes.projects || []) const projects = (parsed.attributes.projects || [])
.filter(project => project.displayed) .filter(project => project.displayed)
.map(parseField('description')) .map(parseField('description'))
const education = (parsed.attributes.education || []) const education = (parsed.attributes.education || [])
.filter(education => education.displayed) .filter(education => education.displayed)
.map(parseField('description')) .map(parseField('description'))
const response = { const response = {
title: parsed.attributes.title, title: parsed.attributes.title,
body: marked(parsed.body), body: marked(parsed.body),
@ -34,10 +37,3 @@ export async function get(req, res, next) {
res.setHeader('Content-Type', 'application/json') res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(response)) res.end(JSON.stringify(response))
} }
function parseField(field) {
return item => ({
...item,
[field]: marked(item[field])
})
}

View File

@ -3,32 +3,36 @@ backend:
repo: michalvankodev/michalvankodev repo: michalvankodev/michalvankodev
branch: master # Branch to update (optional; defaults to master) branch: master # Branch to update (optional; defaults to master)
media_folder: "static/images/uploads" # Media files will be stored in the repo under images/uploads media_folder: 'static/images/uploads' # Media files will be stored in the repo under images/uploads
public_folder: "/images/uploads" # The src attribute for uploaded media will begin with /images/uploads public_folder: '/images/uploads' # The src attribute for uploaded media will begin with /images/uploads
collections: collections:
- name: "blog" # Used in routes, e.g., /admin/collections/blog - name: 'blog' # Used in routes, e.g., /admin/collections/blog
label: "Blog" # Used in the UI label: 'Blog' # Used in the UI
folder: "_posts/blog" # The path to the folder where the documents are stored folder: '_posts/blog' # The path to the folder where the documents are stored
create: true # Allow users to create new documents in this collection create: true # Allow users to create new documents in this collection
slug: "{{year}}-{{month}}-{{day}}-{{slug}}" # Filename template, e.g., YYYY-MM-DD-title.md slug: '{{year}}-{{month}}-{{day}}-{{slug}}' # Filename template, e.g., YYYY-MM-DD-title.md
fields: # The fields for each document, usually in front matter fields: # The fields for each document, usually in front matter
- { label: "Layout", name: "layout", widget: "hidden", default: "blog" } - { label: 'Layout', name: 'layout', widget: 'hidden', default: 'blog' }
- { label: "Title", name: "title", widget: "string" } - { label: 'Title', name: 'title', widget: 'string' }
- { label: "Publish Date", name: "date", widget: "datetime" } - { label: 'Publish Date', name: 'date', widget: 'datetime' }
- { label: "Featured Image", name: "thumbnail", widget: "image" } - { label: 'Featured Image', name: 'thumbnail', widget: 'image' }
- { label: "Rating (scale of 1-5)", name: "rating", widget: "number" } - { label: 'Tags', name: 'tags', widget: 'list', default: ['News'] }
- { label: "Body", name: "body", widget: "markdown" } - { label: 'Body', name: 'body', widget: 'markdown' }
- name: "pages" - name: 'pages'
label: "Pages" label: 'Pages'
files: files:
- label: "Portfolio" - label: 'Portfolio'
name: "portfolio" name: 'portfolio'
file: "_pages/portfolio.md" file: '_pages/portfolio.md'
fields: fields:
- { label: Title, name: title, widget: string } - { label: Title, name: title, widget: string }
- { label: Body, name: body, widget: markdown } - { label: Body, name: body, widget: markdown }
- { label: Work history prelude, name: work_history_prelude, widget: markdown } - {
label: Work history prelude,
name: work_history_prelude,
widget: markdown,
}
- label: Work history - label: Work history
name: work_history name: work_history
widget: list widget: list
@ -40,19 +44,38 @@ collections:
widget: list widget: list
fields: fields:
- { label: Project name, name: name, widget: string } - { label: Project name, name: name, widget: string }
- { label: Displayed, name: displayed, widget: boolean, default: true } - {
label: Displayed,
name: displayed,
widget: boolean,
default: true,
}
- { label: Description, name: description, widget: markdown } - { label: Description, name: description, widget: markdown }
- label: Image - label: Image
name: image name: image
widget: object widget: object
fields: fields:
- { label: Source, name: source, widget: image, required: false } - {
- { label: Image description, name: image_description, widget: string, required: false } label: Source,
name: source,
widget: image,
required: false,
}
- {
label: Image description,
name: image_description,
widget: string,
required: false,
}
- label: Education - label: Education
name: education name: education
widget: list widget: list
fields: fields:
- { label: Institution, name: name, widget: string } - { label: Institution, name: name, widget: string }
- { label: Displayed, name: displayed, widget: boolean, default: true } - {
label: Displayed,
name: displayed,
widget: boolean,
default: true,
}
- { label: Description, name: description, widget: markdown } - { label: Description, name: description, widget: markdown }

View File

@ -74,7 +74,7 @@ p {
} }
::selection { ::selection {
background: #25b5c5; background: #0dd0d0;
} }
@media only screen and (min-width: 400px) { @media only screen and (min-width: 400px) {