Skip to content

Commit 7bb5e2f

Browse files
Hyeok KimHyeok Kim
authored andcommitted
blog
1 parent a4b12ef commit 7bb5e2f

100 files changed

Lines changed: 1613 additions & 4 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,46 @@ Go to `featured-venues.json` and add to the bottom. Make sure that you are using
6767
- How do I deploy new changes?
6868

6969
Create a new branch (`git checkout -b my-branch-name`), commit as normal, then push your branch and open a PR. When your change lands on `main` it will be deployed to the web automatically.
70+
71+
- How do I add a new blog post?
72+
73+
Create a markdown file under `static/blog-assets/posts`. The filename will be the slug in the URL. It can technically be anything, but the convention is `year-month-date-keyword.md` for consistency. Write a post by taking the following steps:
74+
75+
1. Write meta data for sorting and linking. At the top of your markdown file, write meta data between `---` and `---` in YAML format.
76+
77+
```yaml
78+
---
79+
date: 2017-10-31 # required. year-month-date
80+
title: "Introducing Vega-Lite 2.0" # required. in quotes
81+
banner: "../blog-assets/images/2017-10-31-vegalite2-banner.webp" # optional. if provided, it appears before the title.
82+
paper: vega-lite # optional paper keyword. if provided, it will create a link to the paper under the title.
83+
headlinener: "..." # optional if you want to have some special summary for your post. Make sure it is about 100 letters. If external is provided (see below), it is required.
84+
external: URL # if it is posted on an external blog, then just provide that url here. While you are still need to say something in the post for parsing purposes, it will be ignored. To do so, headliner must be provided.
85+
---
86+
```
87+
88+
2. Write your post below the meta data. Use common markdown formatting options. Here are some special cases:
89+
90+
a. Image caption:
91+
92+
```
93+
![alt text](image url)
94+
*your caption goes here.*
95+
```
96+
97+
b. Horizontally placing images (the line changes are all intentional):
98+
99+
```
100+
<div class="image image-flex">
101+
102+
![](../blog-assets/images/image-1)
103+
104+
![](../blog-assets/images/image-2)
105+
</div>
106+
107+
*Your caption goes here.*
108+
```
109+
110+
3. Store images in `static/blog-assets/images` directory. For maintenence purposes, name your images starting with your post's file name.
111+
112+
4. Supported headlines `<h2>` (`##`) and `<h3>` (`###`).

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"@types/d3": "^7.4.3",
5757
"d3": "^7.9.0",
5858
"d3-force": "^3.0.0",
59-
"markdown-it": "^14.1.0"
59+
"markdown-it": "^14.1.0",
60+
"yaml": "^2.7.1"
6061
}
6162
}

scripts/integrity-enforcement.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import fs from 'fs/promises';
22
import peopleRaw from '../static/people.json?raw';
3-
import type { Paper, Person } from '../src/lib/app-types';
3+
import type { BlogPost, Paper, Person } from '../src/lib/app-types';
4+
import { parsePostData, stripHTML } from "../src/lib/pasre-post";
5+
import markdownit from 'markdown-it'
46

57
async function generateIndex() {
68
const people = JSON.parse(peopleRaw) as Person[];
@@ -41,7 +43,39 @@ async function generateIndex() {
4143
await fs.writeFile('./static/papers-index.json', JSON.stringify(papers, null, 2));
4244
}
4345

46+
async function generateBlogList() {
47+
const postList = await fs.readdir('./static/blog-assets/posts');
48+
49+
const md = markdownit({ html: true, linkify: true });
50+
51+
const posts = [] as BlogPost[];
52+
for (const post of postList) {
53+
const web_name = post.split(".").slice(0, -1).join(".");
54+
const postRaw = await fs
55+
.readFile(`./static/blog-assets/posts/${post}`, 'utf8')
56+
.then((x) => parsePostData(x, web_name) as BlogPost);
57+
const rendered_post = md.render(postRaw.post)
58+
const summary = stripHTML(rendered_post).slice(0, 100)
59+
const first_image = postRaw.first_image;
60+
posts.push({ meta: postRaw.meta, post: summary, first_image });
61+
}
62+
posts.sort((a, b) => {
63+
const ad = a.meta.date;
64+
const bd = b.meta.date;
65+
const at = a.meta.title;
66+
const bt = b.meta.title;
67+
// sort by reverse mod date, break ties by alphabetic title order
68+
return ad < bd ? 1 : ad > bd ? -1 : at < bt ? -1 : at > bt ? 1 : 0;
69+
});
70+
posts.forEach((a, i) => {
71+
a.meta.recent = (i < 5);
72+
})
73+
await fs.writeFile('./static/blog-index.json', JSON.stringify(posts, null, 2));
74+
}
75+
4476
async function main() {
4577
await generateIndex();
78+
await generateBlogList()
4679
}
80+
4781
main();

src/data-integrity.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from 'vitest';
2-
import type { Paper, Person, Spotlight, FeaturedVenue, News, Venue, Course } from './lib/app-types';
2+
import type { Paper, Person, Spotlight, FeaturedVenue, News, Venue, Course, BlogPostMeta, BlogPost } from './lib/app-types';
33

44
import papersIndexRaw from '../static/papers-index.json?raw';
55
import peopleRaw from '../static/people.json?raw';
@@ -8,6 +8,7 @@ import featuredVenuesRaw from '../static/featured-venues.json?raw';
88
import venuesRaw from '../static/venues.json?raw';
99
import newsRaw from '../static/news.json?raw';
1010
import courseRaw from '../static/courses.json?raw';
11+
import postIndexRaw from "../static/blog-index.json?raw";
1112

1213
import tsj from 'ts-json-schema-generator';
1314
import Ajv from 'ajv';
@@ -20,6 +21,7 @@ const featuredVenues = JSON.parse(featuredVenuesRaw) as FeaturedVenue[];
2021
const venues = JSON.parse(venuesRaw) as Venue[];
2122
const news = JSON.parse(newsRaw) as Paper[];
2223
const courses = JSON.parse(courseRaw) as Course[];
24+
const postIndex = JSON.parse(postIndexRaw) as BlogPostMeta[];
2325

2426
test('Web names should be unique', () => {
2527
const webNames = new Set(papers.map((paper) => paper.web_name));
@@ -67,6 +69,16 @@ test('All papers should have urls for their authors if possible', () => {
6769
expect(updatedPapers).toEqual(papers);
6870
});
6971

72+
test('All blog posts should have date and title', () => {
73+
const postsWithMissingData = postIndex.filter((p) =>
74+
// check if date, title, post (user-provided), and web name (auto-gen) are provided
75+
!(p.meta.date && p.meta.title && p.meta.web_name && p.post)
76+
// check if an external post has a headliner for preview
77+
&& (!p.meta.external || p.meta.headliner)
78+
);
79+
expect(postsWithMissingData).toEqual([]);
80+
});
81+
7082
[
7183
{ key: 'Paper', dataset: papers, accessor: (paper: Paper): string => paper.web_name },
7284
{
@@ -98,7 +110,8 @@ test('All papers should have urls for their authors if possible', () => {
98110
key: 'Course',
99111
dataset: courses as Course[],
100112
accessor: (course: Course): string => course.name
101-
}
113+
},
114+
{ key: 'BlogPost', dataset: postIndex, accessor: (post: BlogPost): string => post.meta.web_name }
102115
].forEach(({ key, dataset, accessor }) => {
103116
test(`All ${key} values should be filled out`, () => {
104117
const ajv = new Ajv({ allErrors: true });

src/lib/app-types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,21 @@ export type Venue = {
8383
// venueType: 'C' | 'J' | 'B' | 'W';
8484
venueType: 'conference' | 'journal' | 'book' | 'workshop';
8585
};
86+
87+
export type BlogPost = {
88+
meta: BlogPostMeta;
89+
post: string;
90+
first_image?: string | null;
91+
};
92+
93+
export type BlogPostMeta = {
94+
date: string;
95+
display_date: string;
96+
title: string;
97+
web_name: string;
98+
recent?: boolean;
99+
headliner?: string;
100+
banner?: string;
101+
paper?: string;
102+
[key: string]: any;
103+
}

src/lib/pasre-post.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { BlogPost, BlogPostMeta } from "./app-types";
2+
import { parse as parseYAML } from 'yaml';
3+
import markdownit from 'markdown-it'
4+
5+
export function parsePostData(text: string, web_name: string): BlogPost {
6+
let parts = text.split("---\n");
7+
let metaRaw = parseYAML(parts.length == 3 ? parts[1] : "") as { [key: string]: any };
8+
if (!metaRaw.title) {
9+
console.error("Untitled blog post.")
10+
}
11+
let meta: BlogPostMeta = {
12+
date: metaRaw.date,
13+
display_date: metaRaw.date ? (new Date(metaRaw.date + " PST")).toLocaleDateString("us-EN", {
14+
year: "numeric",
15+
month: "short",
16+
day: "numeric",
17+
}) : "Undated",
18+
title: metaRaw.title as string,
19+
web_name: web_name,
20+
}
21+
if (metaRaw.banner) meta.banner = metaRaw.banner;
22+
if (metaRaw.headliner) meta.headliner = metaRaw.headliner;
23+
if (metaRaw.external) meta.external = metaRaw.external;
24+
if (metaRaw.paper) meta.paper = metaRaw.paper;
25+
26+
let post = parts.length == 3 ? parts[2] : parts[0];
27+
28+
const md = markdownit({ html: true, linkify: true });
29+
const rendered_post = md.render(post)
30+
const first_image = meta.banner ?? rendered_post.match(/<img[^<>]*src=\"([^<>\"]+)\"[^<>]*>/i)?.[1] ?? null;
31+
32+
return { meta, post: rendered_post, first_image };
33+
}
34+
35+
export function stripHTML(html: string) {
36+
// getting summary text for the blog
37+
return html.replace(/<[^<>]+>/g, "")
38+
}

src/lib/post-thumb.svelte

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<script lang="ts">
2+
import { base } from '$app/paths';
3+
import type { BlogPost } from './app-types';
4+
export let post: BlogPost;
5+
</script>
6+
7+
<a
8+
href={post.meta.external ?? `${base}/blog/${post.meta.web_name}`}
9+
class="flex paper text-[15px] mb-6 thumb-wrap"
10+
target={post.meta.external ? '_blank' : '_self'}
11+
>
12+
{#if post.first_image}
13+
<div class="thumbnail mb-2 md:mt-1 grow-0 shrink-0 mr-5">
14+
<div
15+
class="rounded-lg w-[120px] h-[80px] post-thumb-image"
16+
style={`background-image: url(${base}/${post.first_image})`}
17+
></div>
18+
</div>
19+
{/if}
20+
<div class="leading-tight">
21+
<div class="md:block">
22+
<a class="font-semibold" href={post.meta.external ?? `${base}/blog/${post.meta.web_name}`}
23+
>{post.meta.title}</a
24+
>
25+
<div class="my-2 text-slate-500">{post.meta.headliner ?? post.post}...</div>
26+
<div class="text-[12px] text-slate-400">{post.meta.display_date}</div>
27+
</div>
28+
</div>
29+
</a>
30+
31+
<style>
32+
.post-thumb-image {
33+
display: block;
34+
background-position: center center;
35+
background-size: cover;
36+
}
37+
.thumb-wrap:hover .post-thumb-image {
38+
box-shadow: 2px 2px 18px #8a5ed3;
39+
}
40+
</style>

src/routes/blog/+page.svelte

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script lang="ts">
2+
import type { BlogPost } from '../../lib/app-types';
3+
import PostThumb from '../../lib/post-thumb.svelte';
4+
5+
export let data: { posts: BlogPost[] };
6+
$: posts = data.posts;
7+
</script>
8+
9+
<svelte:head>
10+
<title>UW Interactive Data Lab | Blog</title>
11+
</svelte:head>
12+
13+
<div class="md:pr-10">
14+
{#each posts as post}
15+
<PostThumb {post} />
16+
{/each}
17+
</div>

src/routes/blog/+page.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { base } from '$app/paths';
2+
import type { BlogPost } from '$lib/app-types';
3+
import type { PageLoad } from './$types';
4+
5+
export const load: PageLoad = async ({ fetch }) => {
6+
const posts = await fetch(`${base}/blog-index.json`)
7+
.then((x) => x.json() as Promise<BlogPost[]>);
8+
9+
return { posts };
10+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts">
2+
import { page } from '$app/stores';
3+
console.log('error');
4+
</script>
5+
6+
{#if $page.error && $page.error.message}
7+
<h1>{$page.status}: {$page.error.message}</h1>
8+
{/if}

0 commit comments

Comments
 (0)