Skip to content

Blog for the IDL website #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,65 @@ Go to `featured-venues.json` and add to the bottom. Make sure that you are using
- How do I deploy new changes?

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.

- How do I add a new blog post?

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 (e.g., `2015-12-17-next-steps.md`). In that file, write a post by taking the following steps:

1. Sepcify meta data in YAML format, between `---` and `---`, at the top of your markdown post file. This is necessary for linking and sorting.

```yaml
---
date: 2017-10-31 # required. year-month-date, in numbers.
title: "Introducing Vega-Lite 2.0" # required. in double-quotes
banner: "../blog-assets/images/2017-10-31-vegalite2-banner.webp" # optional. if provided, it appears before the title.
paper: vega-lite # optional. if provided, it will create a link to the paper under the title (in the post page).
headliner: "..." # (1) optional, if your post is not external (i.e., there is content below this meta data section) and you want to have some custom summary for your post
# (2) required, if your post is external (i.e., the below `external' field is provided)
# For both cases, make sure it is about 100 letters for the layout purposes.
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 the parsing purposes (put something like "external post"), it will be ignored. When this field is provided, then the above "headliner" field is required. This will be checked when you run the test script.
---
```

2. Write your post below the meta data. Use common markdown formatting options. Here are some special cases:

a. Image caption:

```
![alt text](image url)
*your caption goes here.*
```

b. Horizontally placing images (the line changes are all intentional):

```
<div class="image image-flex">

![](../blog-assets/images/image-1)

![](../blog-assets/images/image-2)
</div>

*Your caption goes here.*
```

c. A display text:
```
<p class="display">Some text</p>
```

d. A quote (Note: markdown formatting does not work within a `<blockquote>` tag, so any formatting, such as boldface or italic, must be specified using HTML):
```
<blockquote>
Some text <em>italic</em> and <strong>bold</em>
</blockquote>
```

e. A space divider (This will be rendered as a short, center-aligned horizontal line):
```
* * *
```

3. Store images in `static/blog-assets/images` directory. For maintenence purposes, name your images starting with your post's file name.

4. Supported headings `<h2>` (`##`) and `<h3>` (`###`).
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@types/d3": "^7.4.3",
"d3": "^7.9.0",
"d3-force": "^3.0.0",
"markdown-it": "^14.1.0"
"markdown-it": "^14.1.0",
"yaml": "^2.7.1"
}
}
36 changes: 35 additions & 1 deletion scripts/integrity-enforcement.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import fs from 'fs/promises';
import peopleRaw from '../static/people.json?raw';
import type { Paper, Person } from '../src/lib/app-types';
import type { BlogPost, Paper, Person } from '../src/lib/app-types';
import { parsePostData, stripHTML } from "../src/lib/pasre-post";
import markdownit from 'markdown-it'

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

async function generateBlogList() {
const postList = await fs.readdir('./static/blog-assets/posts');

const md = markdownit({ html: true, linkify: true });

const posts = [] as BlogPost[];
for (const post of postList) {
const web_name = post.split(".").slice(0, -1).join(".");
const postRaw = await fs
.readFile(`./static/blog-assets/posts/${post}`, 'utf8')
.then((x) => parsePostData(x, web_name) as BlogPost);
const rendered_post = md.render(postRaw.post)
const summary = stripHTML(rendered_post).slice(0, 100)
const first_image = postRaw.first_image;
posts.push({ meta: postRaw.meta, post: summary, first_image });
}
posts.sort((a, b) => {
const ad = a.meta.date;
const bd = b.meta.date;
const at = a.meta.title;
const bt = b.meta.title;
// sort by reverse mod date, break ties by alphabetic title order
return ad < bd ? 1 : ad > bd ? -1 : at < bt ? -1 : at > bt ? 1 : 0;
});
posts.forEach((a, i) => {
a.meta.recent = (i < 5);
})
await fs.writeFile('./static/blog-index.json', JSON.stringify(posts, null, 2));
}

async function main() {
await generateIndex();
await generateBlogList()
}

main();
17 changes: 15 additions & 2 deletions src/data-integrity.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from 'vitest';
import type { Paper, Person, Spotlight, FeaturedVenue, News, Venue, Course } from './lib/app-types';
import type { Paper, Person, Spotlight, FeaturedVenue, News, Venue, Course, BlogPostMeta, BlogPost } from './lib/app-types';

import papersIndexRaw from '../static/papers-index.json?raw';
import peopleRaw from '../static/people.json?raw';
Expand All @@ -8,6 +8,7 @@ import featuredVenuesRaw from '../static/featured-venues.json?raw';
import venuesRaw from '../static/venues.json?raw';
import newsRaw from '../static/news.json?raw';
import courseRaw from '../static/courses.json?raw';
import postIndexRaw from "../static/blog-index.json?raw";

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

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

test('All blog posts should have date and title', () => {
const postsWithMissingData = postIndex.filter((p) =>
// check if date, title, post (user-provided), and web name (auto-gen) are provided
!(p.meta.date && p.meta.title && p.meta.web_name && p.post)
// check if an external post has a headliner for preview
&& (!p.meta.external || p.meta.headliner)
);
expect(postsWithMissingData).toEqual([]);
});

[
{ key: 'Paper', dataset: papers, accessor: (paper: Paper): string => paper.web_name },
{
Expand Down Expand Up @@ -98,7 +110,8 @@ test('All papers should have urls for their authors if possible', () => {
key: 'Course',
dataset: courses as Course[],
accessor: (course: Course): string => course.name
}
},
{ key: 'BlogPost', dataset: postIndex, accessor: (post: BlogPost): string => post.meta.web_name }
].forEach(({ key, dataset, accessor }) => {
test(`All ${key} values should be filled out`, () => {
const ajv = new Ajv({ allErrors: true });
Expand Down
18 changes: 18 additions & 0 deletions src/lib/app-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,21 @@ export type Venue = {
// venueType: 'C' | 'J' | 'B' | 'W';
venueType: 'conference' | 'journal' | 'book' | 'workshop';
};

export type BlogPost = {
meta: BlogPostMeta;
post: string;
first_image?: string | null;
};

export type BlogPostMeta = {
date: string;
display_date: string;
title: string;
web_name: string;
recent?: boolean;
headliner?: string;
banner?: string;
paper?: string;
[key: string]: any;
}
42 changes: 42 additions & 0 deletions src/lib/pasre-post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { BlogPost, BlogPostMeta } from "./app-types";
import { parse as parseYAML } from 'yaml';
import markdownit from 'markdown-it'

export function parsePostData(text: string, web_name: string): BlogPost {
const parts = text.split("---\n");
const metaRaw = parseYAML(parts.length == 3 ? parts[1] : "") as { [key: string]: any };
if (!metaRaw.title) {
console.error("Untitled blog post.")
}
const meta: BlogPostMeta = {
date: metaRaw.date,
display_date: metaRaw.date ? (new Date(metaRaw.date.replace(/-/g, "/") + " PST")).toLocaleDateString("us-EN", {
year: "numeric",
month: "short",
day: "numeric",
}) : "Undated",
title: metaRaw.title as string,
web_name: web_name,
}
if (metaRaw.banner) meta.banner = metaRaw.banner;
if (metaRaw.headliner) meta.headliner = metaRaw.headliner;
if (metaRaw.external) meta.external = metaRaw.external;
if (meta.external && !meta.headliner) {
console.error("An external post must have a headliner.");
}
if (metaRaw.paper) meta.paper = metaRaw.paper;

const post = parts.length == 3 ? parts[2] : parts[0];

const md = markdownit({ html: true, linkify: true });
const rendered_post = md.render(post)
let first_image = meta.banner ?? rendered_post.match(/<img[^<>]*src="([^<>"]+)"[^<>]*>/i)?.[1] ?? null;
if (first_image && first_image.startsWith("../")) first_image = first_image.replace("../", "");

return { meta, post: rendered_post, first_image };
}

export function stripHTML(html: string) {
// getting summary text for the blog
return html.replace(/<[^<>]+>/g, "")
}
44 changes: 44 additions & 0 deletions src/lib/post-thumb.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script lang="ts">
import { base } from '$app/paths';
import type { BlogPost } from './app-types';
export let post: BlogPost;
</script>

<a
href={post.meta.external ?? `${base}/blog/${post.meta.web_name}`}
class="flex paper text-[15px] mb-6 thumb-wrap"
target={post.meta.external ? '_blank' : '_self'}
>
{#if post.first_image}
<div class="thumbnail mb-2 md:mt-1 grow-0 shrink-0 mr-5">
<div
class="rounded-lg w-[120px] h-[80px] post-thumb-image"
style={`background-image: url(${base}/${post.first_image})`}
></div>
</div>
{:else}
<div class="thumbnail mb-2 md:mt-1 grow-0 shrink-0 mr-5">
<div class="rounded-lg w-[120px] h-[80px]" style={`background-color: white;`}></div>
</div>
{/if}
<div class="leading-tight">
<div class="md:block">
<a class="font-semibold" href={post.meta.external ?? `${base}/blog/${post.meta.web_name}`}
>{post.meta.title}</a
>
<div class="my-2 text-slate-500">{post.meta.headliner ?? post.post}...</div>
<div class="text-[12px] text-slate-400">{post.meta.display_date}</div>
</div>
</div>
</a>

<style>
.post-thumb-image {
display: block;
background-position: center center;
background-size: cover;
}
.thumb-wrap:hover .post-thumb-image {
box-shadow: 2px 2px 18px #8a5ed3;
}
</style>
2 changes: 1 addition & 1 deletion src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
},
{
name: 'blog',
href: 'https://medium.com/@uwdata'
href: `${base}/blog` // 'https://medium.com/@uwdata'
},
{
name: 'code',
Expand Down
17 changes: 17 additions & 0 deletions src/routes/blog/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script lang="ts">
import type { BlogPost } from '../../lib/app-types';
import PostThumb from '../../lib/post-thumb.svelte';

export let data: { posts: BlogPost[] };
$: posts = data.posts;
</script>

<svelte:head>
<title>UW Interactive Data Lab | Blog</title>
</svelte:head>

<div class="md:pr-10">
{#each posts as post}
<PostThumb {post} />
{/each}
</div>
10 changes: 10 additions & 0 deletions src/routes/blog/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { base } from '$app/paths';
import type { BlogPost } from '$lib/app-types';
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ fetch }) => {
const posts = await fetch(`${base}/blog-index.json`)
.then((x) => x.json() as Promise<BlogPost[]>);

return { posts };
};
8 changes: 8 additions & 0 deletions src/routes/blog/[slug]/+error.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import { page } from '$app/stores';
console.log('error');
</script>

{#if $page.error && $page.error.message}
<h1>{$page.status}: {$page.error.message}</h1>
{/if}
Loading