Building a Serverless Blog with StepZen, SvelteKit, and the DEV API
SvelteKit is a serverless first Svelte metaframework for building web applications with filesystem-based routing.
npm init svelte@next stepzen-svelte-devto
Select "Skeleton project."
✔ Which Svelte app template? › Skeleton project
You'll then be asked a series of questions to configure your application. Feel free to answer based on your own personal use case.
✔ Use TypeScript? … No / Yes
✔ Add ESLint for code linting? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
For the sake of simplicity in this example I answered no for TypeScript, ESLint, and Prettier and I will not include any additional CSS libraries.
StepZen Setup
cd stepzen-svelte-devto
mkdir -p stepzen/schema
touch stepzen/index.graphql \
stepzen/schema/user.graphql stepzen/schema/articles.graphql \
stepzen/schema/followers.graphql stepzen/schema/comments.graphql \
stepzen/schema/organizations.graphql stepzen/schema/podcasts.graphql
echo '{"endpoint": "api/stepzen-svelte-devto", "root": "stepzen"}' > stepzen.config.json
echo 'configurationset:' > config.yaml
StepZen Configuration
configurationset:
- configuration:
name: devto_config
devto_api_key: xxxx
# stepzen/index.graphql
schema
@sdl(
files: [
"schema/user.graphql"
"schema/articles.graphql"
"schema/followers.graphql"
"schema/comments.graphql"
"schema/organizations.graphql"
"schema/podcasts.graphql"
]
) {
query: Query
}
# stepzen/schema/user.graphql
type DevToUser {
github_username: String!
id: Int!
joined_at: String!
location: String!
name: String!
profile_image: String!
summary: String!
twitter_username: String!
type_of: String!
username: String!
website_url: String!
}
type Query {
getUser(
id: String!, url: String
): DevToUser
@rest(endpoint: "https://dev.to/api/users/$id")
}
Deploy StepZen Endpoint
stepzen start
query GET_AJCWEBDEV {
getUser(id: "by_username", url: "ajcwebdev") {
name
summary
github_username
twitter_username
location
profile_image
website_url
}
}
Install dependencies and start development server
npm i
npm run dev
Open localhost:3000
to see the project.
Project Structure
Now we'll look at the code.
├── src
│ ├── routes
│ │ └── index.svelte
│ ├── app.html
│ └── global.d.ts
├── static
│ └── favicon.png
├── stepzen
│ ├── schema
│ │ ├── articles.graphql
│ │ ├── comments.graphql
│ │ ├── followers.graphql
│ │ ├── organizations.graphql
│ │ ├── podcasts.graphql
│ │ └── user.graphql
│ └── index.graphql
├── .gitignore
├── .npmrc
├── config.yaml
├── jsconfig.json
├── package-lock.json
├── package.json
├── README.md
├── stepzen.config.json
└── svelte.config.js
Pages
Pages are Svelte components written in .svelte
files (or any file with an extension listed in config.extensions
). By default, when a user first visits the application, they will be served a server-rendered version of the page in question, plus some JavaScript that 'hydrates' the page and initializes a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel where the common portions in the layout do not need to be rerendered.
The filename determines the route. For example, src/routes/index.svelte
is the root of your site.
<!-- src/routes/index.svelte -->
<script></script>
<section></section>
<style></style>
A .svelte
file contains three parts:
<script>
for JavaScript<style>
for CSS- Any markup you want to include with HTML.
Pages typically generate HTML to display to the user (as well as any CSS and JavaScript needed for the page). By default, pages are rendered on both the client and server, though this behavior is configurable.
Endpoints
Endpoints are modules written in .js
(or .ts
) files that export functions corresponding to HTTP methods.
touch src/routes/user.json.js
Endpoints run only on the server (or when you build your site, if pre-rendering). Pages can request data from endpoints. Endpoints return JSON by default, though may also return data in other formats.
// src/routes/user.json.js
export async function post() {}
A component that defines a page or a layout can export a load
function that runs before the component is created and receive an implementation of fetch
.
// src/routes/user.json.js
export async function post() {
const response = await fetch()
const data = await response.json()
if (data) {
return {
body: data
}
}
}
This endpoint and corresponding function can be used to:
- Access cookies on the server
- Make requests against the app's own endpoints without issuing HTTP calls
- Make a copy of the response and then send it embedded in the initial page load for hydration
Since endpoints only run on the server, they can be used for requests with private API keys that can't be exposed on the client. This also means you'll need to set those API keys to environment variables.
npm i -D dotenv
echo 'STEPZEN_ENDPOINT=\nSTEPZEN_API_KEY=' > .env
Include your StepZen endpoint and API keys in .env
.
STEPZEN_ENDPOINT=
STEPZEN_API_KEY=
// src/routes/user.json.js
import 'dotenv/config'
const { STEPZEN_ENDPOINT, STEPZEN_API_KEY } = process.env
export async function post() {
const response = await fetch(STEPZEN_ENDPOINT, {
headers: {
Authorization: `apikey ${STEPZEN_API_KEY}`
}
})
const data = await response.json()
if (data) {
return {
body: data
}
}
}
Fill in the rest of the POST
request with our GraphQL people
query.
// src/routes/user.json.js
import 'dotenv/config'
export async function post() {
const response = await fetch(process.env.STEPZEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `apikey ${process.env.STEPZEN_API_KEY}`
},
body: JSON.stringify({
query: `{
getUser(id: "by_username", url: "ajcwebdev") {
name
summary
github_username
location
profile_image
}
}`
})
})
const data = await response.json()
if (data) {
return {
body: data
}
}
}
Load Function
load
is similar to getStaticProps
or getServerSideProps
in Next.js, except that it runs on both the server and the client.
<!-- src/routes/index.svelte -->
<script context="module">
export const load = async ({ fetch }) => {
try {
const response = await fetch('/user.json', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
}
})
return {
props: { ...(await response.json()) }
}
} catch (error) {
console.error(`Error in load function for /: ${error}`)
}
}
</script>
<script>
export let data
</script>
<main class="container">
<ul class="content">
<h2>{data.getUser.name}</h2>
<h3>{data.getUser.github_username} - {data.getUser.location}</h3>
<p>{data.getUser.summary}</p>
<img src="{data.getUser.profile_image}" alt="profile pic">
</ul>
</main>
The <script context="module">
is necessary because load
runs before the component is rendered. Code that is per-component instance should go into a second <script>
tag.
Articles
# stepzen/schema/articles.graphql
type ArticleShow {
body_html: String!
body_markdown: String!
canonical_url: String!
comments_count: Int!
cover_image: String!
description: String!
organization: DevToOrganization
path: String!
public_reactions_count: Int!
published_at: String!
readable_publish_date: String!
slug: String!
tags: String!
tag_list: JSON
title: String!
url: String!
}
type getArticlesResponse {
body_html: String!
body_markdown: String!
canonical_url: String!
comments_count: Int!
cover_image: String!
created_at: String!
description: String!
organization: DevToOrganization
path: String!
public_reactions_count: Int!
published_at: String!
readable_publish_date: String!
slug: String!
tags: String!
tag_list: JSON
title: String!
url: String!
user: DevToUser!
}
type Query {
getArticleByPath(
slug: String!, username: String!
): ArticleShow
@rest(endpoint: "https://dev.to/api/articles/$username/$slug")
getArticles(
collection_id: Int
page: Int
per_page: Int
state: String
tag: String
tags: String
tags_exclude: String
top: Int
username: String
): [getArticlesResponse]
@rest(endpoint: "https://dev.to/api/articles")
}
query GET_AJCWEBDEV_ARTICLES {
getArticles(username: "ajcwebdev", per_page: 100) {
title
description
readable_publish_date
cover_image
tags
public_reactions_count
slug
url
}
}
Svelte Config
// svelte.config.js
import adapter from '@sveltejs/adapter-netlify';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
split: false
}),
target: '#svelte'
}
};
export default config;
target
will hydrate the <div id="svelte">
element in src/app.html
.
Official adapters for deployment
Svelte apps are built with adapters for optimizing your project to deploy with different environments. This project uses the adapter-netlify
for Netlify.
Other Queries
query GET_STEPZEN_ORG {
getOrganization(username: "stepzen") {
name
summary
username
url
twitter_username
profile_image
}
}
query GET_STEPZEN_ORG_USERS {
getOrgUsers(username: "stepzen") {
name
username
summary
location
website_url
}
}
query GET_STEPZEN_ORG {
getOrgArticles(username: "stepzen") {
title
description
tags
readable_publish_date
canonical_url
cover_image
public_reactions_count
slug
url
}
}
query GET_PODCAST_EPISODES {
getPodcastEpisodes(username: "fsjampodcast", per_page: 100) {
title
path
}
}