Blog

#javascript

#astro

Really cool Static Site Generator focused on HTML-first web development and delivering blazing fast user experience. Check out more at Astro.build.

Really impressive demo. First, this is for single-page applications. Second, there is an API for multi-page applications. Check it out in Chrome Canary and look at the code. I discussed this with my team yesterday. The demo is built on Astro. All that is shipped to the browser is 301kB. Of that 291kB is images. Less than 5.5kB for the document, CSS, and JS. CSS is powering the transitions and only a bit of JS intercepts the navigation event, loads the fragment of HTML, injects it into the DOM, and adds the necessary classes to trigger the animations.

This is a truly impressive demonstration. With very minimal effort, one can use an SSG like Astro— which can run as an SSR too— and deliver a fully working application that requires no JavaScript but progressively enhances to dynamic page transitions with easy— something that is extremely difficult even for SPA libraries— and asynchronous page loading. Only 150 lines of JS are in this project— 150 lines that ship to the browser.

For an old curmudgeonly standards guy like myself, this gives me some hope that we can get back to the days of the largest assets we send to the browser are images instead of hundreds of kilobytes of JavaScript.

Source: Bramus

Astro Build Performance

Sixty-five seconds just to build out the first pages of the #tech pages. Why? Because they are reliant on a complex function that gets a tag map. So let’s understand how pages are built for a route, from what I gather.

function doComplexThing() {
	const things = [];
	// do the thing

	// return the data
	return data;
}

The getStaticPaths method returns an array of pages and their data. This is called once. So that complex function? Not really a problem here. Then Astro renders each page that getStaticPaths returns. The problem is that I need that complex function’s data here too. Without calling it here, we build these pages in 10 seconds. With it, 65.

export async function getStaticPaths() {
	const things = doComplexThing();
	return things.map(thing => ({
		params: {
			tag: thing
		}
	}));
}

Okay, now we have options. We could just put that data in the paths’ props. But it is used by a component in the page that is rendered. We could pass that data as a prop to that component. But it would be easier to call it inside the component. 65 seconds. And that’s just a fraction of the pages that need to build.

And that leads to an old trick from my PHP days. Caching the data of a function and returning it the next time we try to access it.

export let cachedThings: any[] | undefined = undefined;
export function doComplexThing() {
	if (cachedThings) return cachedThings;
	
	const things = [];
	// do the thing
	
	// cache it
	cachedThings = things;

	// return the data
	return data;
}

Now when we call doComplexThing() the first time, it runs the complex code. But then we hit it hundreds of more times and returns the cache.

Front Matter CMS

With the relaunch of Finley, I am. a couple of weeks ago I shifted from using a CMS to everything-is-code. My blogging days stretch back to the mid-2000’s and my having built my own blogging tool called Blog Wizard. Various versions of that powered several blogs until I launched Finley, I am. in 2015 on Ghost.

Simply put, I am used to having a CMS. Code is great, but certain things are nicer with a CMS. Managing data things, specifically. The week with the relaunch I had imported all the articles from Ghost 1-to-1. Same tags and everything else. Last Sunday I started refactoring the tags. Because everything-is-code and I realized that I could build something cool if my tags were better. As detailed in the linked article, I merged and deleted a ton of tags. Over 300 tags in over 400 posts.

That brings us to Front Matter CMS. First, it’s a plugin for VS Code. Many CMSs exist for SSGs like Astro that write code. This sits alongside your Markdown post, living in the sidebar of Code. For SEO-minded folks, it provides SEO status info— like title, slug, article length, keyword management, etc. For me, it’s the publishing date, draft status, and very much the tag management for articles I love. Autocomplete on tags will help you remain consistent on your tag use, which I then use for finding similar posts.

And it helps you manage. The Dashboard shows all published posts and easily helps you find and edit your drafts. Taxonomies? Yeah, merging, renaming, deleting, and more. And remember, everything-is-code, so changing merging “video” into “videos” results in 10 changed files that you then commit and push.

If you use Hugo, Jekyll, Astro, or other SSGs that use front matter for metadata around posts, go grab Front Matter CMS and give it a shot.

Nothing too big, but I tweaked the “Other Posts You May Enjoy” section to sort by number of matches, not just date. Before it found all posts with one or more of the current post’s tags, but didn’t change the default order of reverse chronological. Now it sorts by number of similar tags, then date. So a post with 3 matches from last year will rank higher than a post with 2 from last week. Nothing ground breaking, but hopefully will help surface more relevant posts.

export async function getSimilarPosts (post: CollectionEntry<'posts'>): Promise<CollectionEntry<'posts'>[]> {
	const excludedTags = ['notes', 'links', 'featured', 'videos', 'quotes']
	const similarTags = post.data.tags.filter(tag => !excludedTags.includes(tag))

	return (
		(await getPosts(filteredPost => (
			filteredPost.slug !== post.slug &&
      filteredPost.data.tags.filter(tag => similarTags.includes(tag)).length > 0
		))).map(post => ({
			...post,
			similarTagCount: post.data.tags.filter(tag => similarTags.includes(tag)).length
		})).sort((a, b) => {
			if (a.similarTagCount > b.similarTagCount) return -1
			if (b.similarTagCount > a.similarTagCount) return 1

			if (a.data.pubDate > b.data.pubDate) return -1
			if (a.data.pubDate < b.data.pubDate) return 1

			return 0
		})
	)
}

I added a new type of post called reposts. In my Article component— used to render each article in multiple modes (eg. list, full, snippet)— I already include all tags as classes. I use this in lists to add the icons— look at the Ramblin’ section of the homepage.

<article data-mode={mode} data-draft={draft} class={`b--post ${tags.map(tag => `tag-${tag}`).join(' ')}`}>
[...]
</article>

So adding a new post type is just a tag. With the class tag-repost, I can now add the icon. But I also wanted to show the original post embedded. To do this, I added an optional string called originalPostSlug to my Content Collection. And this gets added to the Article component.

---
let originalPost: CollectionEntry<'posts'> | undefined = undefined
if (originalPostSlug) {
	originalPost = (await getCollection('posts')).find(post => post.slug === originalPostSlug)
}
---

Now I have the original article. So now we display it after the content.

<div class="b--post--content">
  <Content />
  {
    originalPostSlug && originalPostSlug && (
      <div class="b--post--repost">
        <Article post={originalPost} mode="list" />
      </div>
    )
  }
</div>

You can see it in action here.