Using Turso to serve a Server-side Rendered Astro blog's content
Set up and use Turso as the data API for your Server-side Rendered Astro blogs.
Astro is a ui-agnostic, edge-ready, all-in-one web framework for building fast, content-focused websites that comes with the new component islands architecture useful for building faster websites.
In this tutorial, we are going to create a Server Side Rendered blog using Astro that obtains its data from Turso, the open-source edge database powered by libSQL.
With the edge readiness of Turso, this stack combination would make for a perfect choice when it comes to building fast blogs for your audience.
Let's start building.
# Crafting the blog
N.B., before proceeding, make sure that you have the latest LTS version of Node.js installed in your machine. Also, the final source code for the blog site being built in this tutorial can be found in this GitHub repository for reference.
To create a new Astro project, run the following command.
npm create astro@latest
Follow the installation prompt, filling in the app name when requested and choosing the blog template as the starter.
By now, this should be the project's directory structure.
├── README.md
├── astro.config.mjs
├── package-lock.json
├── package.json
├── public
├── src
│ ├── components
│ ├── consts.ts
│ ├── content
│ ├── env.d.ts
│ ├── layouts
│ ├── pages
│ └── styles
For this post, our focus will be on the components
, layouts
, and pages
directories within the /src directory. For more information on the directory structure, read the Astro docs.
Inside the project directory, run npm install
to install the dependencies, followed by npm run dev
to initiate the dev server. Open http://localhost//:3000
on your browser to see the starter site.
You should see the following page when you visit the locally served blog.
Astro blog initial development preview.
Congrats, you are now an astronaut. Now, let's get into deep space.
Since we are going for a Server Side Rendering (SSR) set up in this blog with Turso serving the data to our Astro routes (using slugs) instead of the default static site builder, we'll need to install an Astro adapter for SSR support.
For demonstration, let's install the Netlify SSR adapter. You can opt for any of the supported SSR adapters depending on the service you are going to use to deploy this blog.
Run the npm run astro add netlify
command to add the adapter and enable SSR in the project. This command will install the adapter and make the required changes in the Astro project configuration file astro.config.mjs
.
# Customizing the blog template
Follow these steps to modify and clean up the starter template for the blog we're building.
- Head over to
src/components/Header.astro
and remove theTwitter
,GitHub
, andBlog
links. - Delete the
/src/pages/blog
and/src/content
directories. - Add a new
/src/pages/post
directory, and inside it add a[slug].astro
file with the following code. - Add a new
/src/pages/post
directory, and inside it add a[slug].astro
file with the following code.
---
const title = "Blog Ppost";
---
<h1> {title} </h1>
- Update the default time formatter in the blog template
./src/components/formattedDate.astro
with code found in this file. - Since we'll be using a custom blog data schema and not Astro's default, replace the component script of the blog layout file
src/layout/BlogPost.astro
with the following.
import BaseHead from '../components/BaseHead.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import FormattedDate from '../components/FormattedDate.astro';
const { title, description, publish_date, created_at, hero, author } = Astro.props;
- Within the same component, update the template markup by replacing the
article
tag with the following code.
<article>
{hero && <img width={720} height={360} src={hero} alt="" />}
<h1 class="title">{title}</h1>
<span class="author">{author.first_name} {author.last_name}.</span>
<FormattedDate date={publish_date | created_at} />
<hr />
<slot />
</article>
# Preparing the database
Let's proceed to prepare the database for the blog's consumption by installing the Turso CLI on your machine.
Afterwards, create the database using the following command.
turso db create my-blog
Then, switch to the SQLite shell by running the command: turso db shell my-blog
. Prepare the blog's database tables and indexes using the SQL statements found here.
Having prepared the database and seeded some data into it, head back to the Astro project and configure it to consume the database data.
# Consuming Turso data within Astro
To consume Turso's data within Astro, we need to first install the libSQL TypeScript driver using the following command.
npm install @libsql/client
Create a new TypeScript file ./src/lib/tursoDb.ts
, and export a Turso client from it.
import { createClient } from "@libsql/client/web";
export const client = createClient({
url: import.meta.env.TURSO_DB_URL,
authToken: import.meta.env.TURSO_DB_AUTH_TOKEN
});
As we can see in the above block code, the client needs a URL
and authToken
arguments which are passed using the TURSO_DB_URL
and TURSO_DB_TOKEN
environment variables.
Let's provide these two credentials by first creating a .env
file at the project root followed by adding the two variables.
TURSO_DB_URL=
TURSO_DB_AUTH_TOKEN=
To obtain the Turso database URL, run turso db show my-blog — url
on the terminal and assign the resulting string to TURSO_DB_URL in the .env file above.
Likewise, for the database token, run turso db tokens create my-blog
, copy the resulting string and assign it to TURSO_DB_AUTH_TOKEN
inside the .env
file.
We also need to declare some types for the project which mirror the data within our database. Do so by creating a types.ts
file under ./src/lib
and add the following code in it.
export interface Author {
id?: number;
first_name: string;
last_name: string;
bio?: string;
slug: string;
email?: string;
socials?: Socials;
youtube?: string;
avatar: string;
created_at?: number;
posts?: Blog[];
}
export interface Blog {
id?: number;
title: string;
description: string;
slug: string;
content?: string;
hero: string;
author?: Author;
created_at: number;
publish_date?: number;
published?: boolean;
}
export interface Socials {
twitter?: string;
website?: string;
}
# Displaying blog data
Replace the code inside the blog's home page: ./src/pages/index.astro
, with the following.
---
import BaseHead from '../components/BaseHead.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
import type { Blog } from '../lib/types';
import { client } from '../lib/tursoDb';
import FormattedDate from "../components/FormattedDate.astro";
let posts: Blog[] = [];
try {
const allPostsResponse = await client.execute("select posts.title, posts.description, posts.slug, posts.hero, authors.first_name, authors.last_name, authors.slug as author_slug, authors.avatar, posts.content, posts.created_at from posts left join authors on authors.id = posts.author_id order by posts.created_at desc;");
const allPosts = allPostsResponse.rows;
posts = allPosts.map((post: any): Blog => {
return {
published: false,
title: post.title,
description: post.description,
slug: post.slug,
hero: post.hero,
created_at: post.created_at,
author: {
first_name: post.first_name,
last_name: post.last_name,
slug: post.slug,
avatar: post.avatar
}
}
});
} catch (error) {
// TODO: Handle error and notify user
}
---
<!DOCTYPE html>
<html lang="en">
<head>
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
<style> /* add your styles */ </style>
</head>
<body>
<Header />
<main>
<section>
<ul>
{
posts.map((post) => (
<li>
<FormattedDate date={post.publish_date || post.created_at} />
<a href={`/post/${post.slug}/`}>{post.title}</a>
</li>
))
}
</ul>
</section>
</main>
<Footer />
</body>
</html>
In this code block, we are fetching the blog post data from Turso and creating a blog post listing on this page.
When you visit this page on the browser, you should be able to see the following.
Back in the blog post file we created earlier: ./src/pages/post/[slug].astro
, replace the existing code with the following.
---
import { Markdown } from "@astropub/md"
import BlogPost from '../../layouts/BlogPost.astro';
import { client } from '../../lib/tursoDb';
import type { Blog } from '../../lib/types';
const { slug } = Astro.params;
let post: Blog | null = null;
if(!slug){
return new Response(null, {
status: 404,
statusText: 'Not found'
});
}
try {
const postResponse = await client.execute({
sql: "select posts.content, posts.published, posts.title, posts.description, posts.slug, posts.hero, posts.created_at, authors.first_name, authors.last_name, authors.slug, authors.avatar, authors.twitter from posts left join authors on authors.id = posts.author_id where posts.slug = ?;",
args: [slug as string],
});
if(!postResponse.rows.length){
return new Response(null, {
status: 404,
statusText: 'Not found'
});
}
const blogPostData = postResponse.rows[0] as any;
if(!blogPostData){
return new Response(null, {
status: 404,
statusText: 'Not found'
});
}
post = {
content: blogPostData.content,
published: blogPostData.published,
title: blogPostData.title,
description: blogPostData.description,
slug: blogPostData.slug,
hero: blogPostData.hero,
created_at: blogPostData.created_at,
author: {
first_name: blogPostData.first_name,
last_name: blogPostData.last_name,
slug: blogPostData.slug,
avatar: blogPostData.avatar,
email: blogPostData.email,
socials: {
twitter: blogPostData.twitter
},
created_at: blogPostData.created_at
}
};
} catch (error) {
// TODO: Handle error and notify user
}
---
<BlogPost {...post}>
<Markdown of={post?.content || ""} />
</BlogPost>
In this code block, we extract the page slug
from the page route parameters, we then proceed to fetch the blog post data from Turso and display it on the page.
Finally, to populate the blog's RSS feed, update the code within ./src/pages/rss.xml
with the following.
import rss from '@astrojs/rss';
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
import { client } from '../lib/tursoDb';
import type { APIContext } from 'astro';
let posts: any[] = [];
try {
const allPostsResponse = await client.execute({
sql: 'select posts.title, posts.description, posts.slug, posts.hero, authors.first_name, authors.last_name, authors.slug as author_slug, authors.avatar, posts.content, posts.created_at from posts left join authors on authors.id = posts.author_id order by posts.created_at desc;',
args: [],
});
posts = allPostsResponse.rows.map((post) => {
return {
published: false,
title: post.title,
description: post.description,
slug: post.slug,
hero: post.hero,
created_at: post.created_at,
publish_date: post.publish_date,
author: {
first_name: post.first_name,
last_name: post.last_name,
slug: post.slug,
avatar: post.avatar,
},
};
});
} catch (error) {
// TODO: Handle error and notify user
}
export async function get(context: APIContext) {
return rss({
title: SITE_TITLE,
description: SITE_DESCRIPTION,
site: context.site as unknown as string,
items: posts.map((post) => ({
title: post.title,
pubDate: new Date((post.publish_date || post.created_at) * 1000),
description: post.description,
link: `/post/${post.slug}/`,
})),
});
}
As we did with the blog listing on the home page, we are fetching all the blog posts and formatting the post data to fit the RSS schema in the above code block.
To preview the RSS feed, open http://localhost://3000/rss.xml
on your browser, and you should see the following preview.
With the SSR setup for an Astro blog and fetching data from Turso, we not only get the benefit of a fast website, we also avoid the need to push to publish/update with the use of local markdown files or abide to specific Content Managing System's (CMS) mode of content submission to add/modify a blog's content (which is all fine).
With this stack, you get to select your mode of choice for content submission. If that means making SQL statements using the Turso CLI or creating a form in whichever framework you fancy (your own in-house CMS) it's up to you, the only limit here is your imagination. And, that is the reason content submission is not part of this tutorial.
For more information regarding today's stack, visit the following links.
Get creative, and get to building.
Stay edgy 😎.