We are going to create a Blog using dev.to API for retrieving our posts and finally deploy it with Now.
Demo: https://jpblanco.dev
Source: https://github.com/JPBlancoDB/svelte-blog
First step: Scaffolding our app.
Let's scaffold our new project with Sapper by cloning their repo as their official guide dictates:
npx degit "sveltejs/sapper-template#rollup" my-app
# or: npx degit "sveltejs/sapper-template#webpack" my-app
cd my-app
npm install
npm run dev
In this case, I've chosen webpack but it is really up to you. This is not going to affect the rest of the post neither I'm going to explain differences between bundlers.
Then, it would be:
npx degit "sveltejs/sapper-template#webpack" svelte-blog
cd svelte-blog
npm install
npm run dev
If we go to http://localhost:3000 we should see that everything is working as expected (and also Borat π).
Lastly, let's remove all the files we are not going to use (at least, it is not within this article).
rm -rf cypress cypress.json src/components/Nav.svelte src/routes/blog/ src/routes/about.svelte
Second step: Fix the base template
As we've removed src/components/Nav.svelte
, we should fix the layout by removing the dependency in our _layout.svelte
file.
<style>
main {
position: relative;
max-width: 56em;
background-color: white;
padding: 2em;
margin: 0 auto;
box-sizing: border-box;
}
</style>
<main>
<slot />
</main>
And we could also add a global title for our blog, this could be achieved by adding svelte:head
tag to our component:
<svelte:head>
<title>Juan Pablo Blanco - Blog</title>
</svelte:head>
The svelte:head element allows you to insert elements inside the
<head>
of your document.
You could read more about svelte:head
in the official doc: Svelte Head
So the end result would be:
<style>
main {
position: relative;
max-width: 56em;
background-color: white;
padding: 2em;
margin: 0 auto;
box-sizing: border-box;
}
</style>
<svelte:head>
<title>Juan Pablo Blanco - Blog</title>
</svelte:head>
<main>
<slot />
</main>
Third step: Home template
This blog is going to be really simple and we only need an index. My idea behind this is just to fetch the article's summary from the API and then show these in our home with a link to dev.to.
I've created a quick wireframe using Wireframe as reference:
So, let's start by doing the template for our articles:
//src/routes/index.svelte
<a href="/">
Oct 10
<h1>Svelte Create the Blog</h1>
<div class="tags">
<span class="tag">#svelte</span>
<span class="tag">#javascript</span>
</div>
<p>How to create a blog using dev.to API and svelte</p>
<img
alt="Reactions"
src="https://practicaldev-herokuapp-com.freetls.fastly.net/assets/reactions-stack-ee166e138ca182a567f74c986b6f810f670f4d199aca9c550cc7e6f49f34bd33.png" />
<span>10</span>
<img
alt="Comments"
src="https://practicaldev-herokuapp-com.freetls.fastly.net/assets/comments-bubble-9958d41b969a1620c614347d5ad3f270ab49582c1d9f82b617a6b4156d05dda0.png" />
<span>3</span>
</a>
You will notice that is not the same as our wireframe. Let's add some CSS to our template.
<style>
a {
text-decoration: none;
}
.card {
padding: 20px;
margin-bottom: 10px;
border: 2px solid #bababa;
box-shadow: 3px 3px 0px #bababa;
}
.tags {
margin: 10px 0;
}
.tag {
border-radius: 100px;
background-color: #eaeaea;
padding: 2px 8px;
margin-right: 10px;
}
@media (max-width: 640px) {
.tag {
margin-right: 5px;
font-size: 0.6rem;
}
}
.article-engagement {
margin-right: 20px;
}
.article-engagement img {
height: 20px;
min-width: 26px;
vertical-align: -5px;
}
.flex-container {
display: flex;
}
</style>
<a href="/">
<div class="card">
Nov 24
<h1>Create a blog with Svelte and DEV.to API</h1>
<div class="tags">
<span class="tag">#svelte</span>
<span class="tag">#javascript</span>
<span class="tag">#tutorial</span>
<span class="tag">#blog</span>
</div>
<p>How to create a blog using dev.to API and svelte</p>
<div class="flex-container">
<div class="article-engagement">
<img
alt="Reactions"
src="https://practicaldev-herokuapp-com.freetls.fastly.net/assets/reactions-stack-ee166e138ca182a567f74c986b6f810f670f4d199aca9c550cc7e6f49f34bd33.png" />
<span>10</span>
</div>
<div class="article-engagement">
<img
alt="Comments"
src="https://practicaldev-herokuapp-com.freetls.fastly.net/assets/comments-bubble-9958d41b969a1620c614347d5ad3f270ab49582c1d9f82b617a6b4156d05dda0.png" />
<span>3</span>
</div>
</div>
</div>
</a>
Ok, I think now is better.
Fourth step: Get articles from the API
Let's use DEV.to API for fetching all our articles, this way we are going to publish our posts in DEV.to platform and automatically is going to be shown in our blog πͺ π.
We could find documentation on how to use the API here: Docs API
For summarize, if we only want to fetch articles we could just do a GET request without any extra authentication or step:
curl https://dev.to/api/articles?username=jpblancodb
The API response is:
[
{
"type_of": "article",
"id": 194541,
"title": "There's a new DEV theme in town for all you 10x hackers out there (plus one actually useful new feature)",
"description": "",
"cover_image": "https://res.cloudinary.com/practicaldev/image/fetch/s--74Bl23tz--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://res.cloudinary.com/practicaldev/image/fetch/s--xU8cbIK4--/c_imagga_scale%2Cf_auto%2Cfl_progressive%2Ch_420%2Cq_auto%2Cw_1000/https://thepracticaldev.s3.amazonaws.com/i/8a39dzf3oovzc2snl7iv.png",
"readable_publish_date": "Oct 24",
"social_image": "https://res.cloudinary.com/practicaldev/image/fetch/s--SeMxdKIa--/c_imagga_scale,f_auto,fl_progressive,h_500,q_auto,w_1000/https://res.cloudinary.com/practicaldev/image/fetch/s--xU8cbIK4--/c_imagga_scale%2Cf_auto%2Cfl_progressive%2Ch_420%2Cq_auto%2Cw_1000/https://thepracticaldev.s3.amazonaws.com/i/8a39dzf3oovzc2snl7iv.png",
"tag_list": [
"meta",
"changelog",
"css",
"ux"
],
"tags": "meta, changelog, css, ux",
"slug": "there-s-a-new-dev-theme-in-town-for-all-you-10x-hackers-out-there-plus-one-actually-useful-new-feature-2kgk",
"path": "/devteam/there-s-a-new-dev-theme-in-town-for-all-you-10x-hackers-out-there-plus-one-actually-useful-new-feature-2kgk",
"url": "https://dev.to/devteam/there-s-a-new-dev-theme-in-town-for-all-you-10x-hackers-out-there-plus-one-actually-useful-new-feature-2kgk",
"canonical_url": "https://dev.to/devteam/there-s-a-new-dev-theme-in-town-for-all-you-10x-hackers-out-there-plus-one-actually-useful-new-feature-2kgk",
"comments_count": 37,
"positive_reactions_count": 142,
"collection_id": null,
"created_at": "2019-10-24T13:41:29Z",
"edited_at": "2019-10-24T13:56:35Z",
"crossposted_at": null,
"published_at": "2019-10-24T13:52:17Z",
"last_comment_at": "2019-10-25T08:12:43Z",
"published_timestamp": "2019-10-24T13:52:17Z",
"user": {
"name": "Ben Halpern",
"username": "ben",
"twitter_username": "bendhalpern",
"github_username": "benhalpern",
"website_url": "http://benhalpern.com",
"profile_image": "https://res.cloudinary.com/practicaldev/image/fetch/s--Y1sq1tFG--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://thepracticaldev.s3.amazonaws.com/uploads/user/profile_image/1/f451a206-11c8-4e3d-8936-143d0a7e65bb.png",
"profile_image_90": "https://res.cloudinary.com/practicaldev/image/fetch/s--DcW51A6v--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://thepracticaldev.s3.amazonaws.com/uploads/user/profile_image/1/f451a206-11c8-4e3d-8936-143d0a7e65bb.png"
},
"organization": {
"name": "The DEV Team",
"username": "devteam",
"slug": "devteam",
"profile_image": "https://res.cloudinary.com/practicaldev/image/fetch/s--0kDBq1Ne--/c_fill,f_auto,fl_progressive,h_640,q_auto,w_640/https://thepracticaldev.s3.amazonaws.com/uploads/organization/profile_image/1/0213bbaa-d5a1-4d25-9e7a-10c30b455af0.png",
"profile_image_90": "https://res.cloudinary.com/practicaldev/image/fetch/s--8tTU-XkZ--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://thepracticaldev.s3.amazonaws.com/uploads/organization/profile_image/1/0213bbaa-d5a1-4d25-9e7a-10c30b455af0.png"
}
}
]
Let's add this API call into our index.svelte
to fetch all our articles by adding a script
tag:
<script context="module">
export async function preload() {
const res = await this.fetch(
`https://dev.to/api/articles?username=jpblancodb`
);
return { articles: await res.json() };
}
</script>
<script>
export let articles;
</script>
We are using preload
, this loads data before the component gets rendered, then we return { articles }
and this is going to be passed as a prop to the component export let articles;
More about preload: Sapper preload
From the response of the API we are only going to use the following properties:
[
{
"title": "There's a new DEV theme in town for all you 10x hackers out there (plus one actually useful new feature)",
"description": "",
"readable_publish_date": "Oct 24",
"tag_list": [
"meta",
"changelog",
"css",
"ux"
],
"canonical_url": "https://dev.to/devteam/there-s-a-new-dev-theme-in-town-for-all-you-10x-hackers-out-there-plus-one-actually-useful-new-feature-2kgk",
"comments_count": 37,
"positive_reactions_count": 142
}
]
Finally, we need to make our template dynamic.
<script context="module">
export async function preload() {
const res = await this.fetch(
`https://dev.to/api/articles?username=jpblancodb`
);
return { articles: await res.json() };
}
</script>
<script>
export let articles;
</script>
<style>
a {
text-decoration: none;
}
.card {
padding: 20px;
margin-bottom: 10px;
border: 2px solid #bababa;
box-shadow: 3px 3px 0px #bababa;
}
.tags {
margin: 10px 0;
}
.tag {
border-radius: 100px;
background-color: #eaeaea;
padding: 2px 8px;
margin-right: 10px;
}
.article-engagement {
margin-right: 20px;
}
.article-engagement img {
height: 20px;
min-width: 26px;
vertical-align: -5px;
}
@media (max-width: 640px) {
.tag {
margin-right: 5px;
font-size: 0.6rem;
}
}
</style>
{#each articles as article}
<a href={article.canonical_url}>
<div class="card">
{article.readable_publish_date}
<h1>{article.title}</h1>
<div class="tags">
{#each article.tag_list as tag}
<span class="tag">#{tag}</span>
{/each}
</div>
<p>{article.description}</p>
<div class="flex-container">
<div class="article-engagement">
<img
alt="Reactions"
src="https://practicaldev-herokuapp-com.freetls.fastly.net/assets/reactions-stack-ee166e138ca182a567f74c986b6f810f670f4d199aca9c550cc7e6f49f34bd33.png" />
<span>{article.positive_reactions_count}</span>
</div>
<div class="article-engagement">
<img
alt="Comments"
src="https://practicaldev-herokuapp-com.freetls.fastly.net/assets/comments-bubble-9958d41b969a1620c614347d5ad3f270ab49582c1d9f82b617a6b4156d05dda0.png" />
<span>{article.comments_count}</span>
</div>
</div>
</div>
</a>
{/each}
Let's modify our global.css
for customizing our styles;
body {
margin: 0;
font-family: Roboto, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
font-size: 16px;
line-height: 1.5;
color: #333;
}
h1 {
margin: 0;
font-weight: 400;
line-height: 1.2;
font-size: 2rem;
}
p {
font-size: 1.2rem;
}
a {
color: inherit;
text-decoration: none;
}
.flex-container {
display: flex;
}
@media (max-width: 640px) {
body {
font-size: 14px;
}
h1 {
font-size: 1.2rem;
margin: 0;
}
p {
font-size: 0.9rem;
line-height: normal;
}
}
Fifth step: Adding a header to our template
For getting our blog ready, we could add a header with our name, description. As this is going to be static, you could just add whatever you prefer. Here is mine! If you want to share yours, you could do it in the comments section π!
//src/components/Header.svelte
<style>
header {
padding: calc(1.4vw + 8px) 0px;
position: relative;
overflow: hidden;
width: 72em;
max-width: 92%;
margin: 30px auto 20px;
background: #fff;
background: var(--theme-container-background, #fff);
border-radius: 3px;
border: 2px solid #551029;
box-shadow: 5px 6px 0px #551029;
}
.profile {
display: flex;
align-items: center;
margin: 0 20px;
}
.profile-image {
background: #61122f;
width: calc(12.2vw + 50px);
height: calc(12.2vw + 50px);
border-radius: 200px;
max-width: 220px;
max-height: 220px;
border: 4px solid #61122f;
}
.icon-img {
margin-right: 1em;
width: 35px;
height: 35px;
}
.profile-description {
display: flex;
justify-content: center;
flex-direction: column;
}
.role {
font-style: italic;
font-size: 1.4rem;
margin: 10px 0;
color: #999999;
}
.social {
margin-top: 25px;
color: #61122f;
}
h1 {
color: #61122f;
font-weight: 600;
}
@media (max-width: 640px) {
.role {
font-size: 1rem;
}
.social {
margin-top: 5px;
}
.icon-img {
margin-right: 1em;
width: 25px;
height: 25px;
}
}
</style>
<header>
<div class="flex-container">
<div class="profile">
<img
class="profile-image"
src="https://res.cloudinary.com/practicaldev/image/fetch/s--122RH5V_--/c_fill,f_auto,fl_progressive,h_320,q_auto,w_320/https://thepracticaldev.s3.amazonaws.com/uploads/user/profile_image/16453/b219d515-8ee9-4f1d-9541-e4e49ce8d066.png"
alt="jpblancodb profile" />
</div>
<div class="profile-description">
<h1>Juan Pablo Blanco</h1>
<span class="role">Software Developer</span>
<div class="social">
<a href="https://twitter.com/jpblancodb" target="_blank">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 612 612"
role="img"
aria-labelledby="a3nwio0iehsz7gmmysvll02oakahlvob"
class="icon-img">
<title id="a3nwio0iehsz7gmmysvll02oakahlvob">Twitter logo</title>
<path
d="M612 116.258c-22.525 9.98-46.694 16.75-72.088 19.772
25.93-15.527 45.777-40.155 55.184-69.41-24.322 14.378-51.17
24.82-79.775 30.48-22.906-24.438-55.49-39.66-91.63-39.66-69.333
0-125.55 56.218-125.55 125.514 0 9.828 1.11 19.427 3.25
28.606-104.325-5.24-196.834-55.223-258.75-131.174-10.822
18.51-16.98 40.078-16.98 63.1 0 43.56 22.182 81.994 55.836
104.48-20.575-.688-39.926-6.348-56.867-15.756v1.568c0 60.806 43.29
111.554 100.692 123.104-10.517 2.83-21.607 4.398-33.08 4.398-8.107
0-15.947-.803-23.634-2.333 15.985 49.907 62.336 86.2 117.253
87.194-42.946 33.655-97.098 53.656-155.915 53.656-10.134
0-20.116-.612-29.944-1.72 55.568 35.68 121.537 56.484 192.44
56.484 230.947 0 357.187-191.29 357.187-357.188l-.42-16.253C573.87
163.525 595.21 141.42 612 116.257z" />
</svg>
</a>
<a href="https://github.com/jpblancodb" target="_blank">
<svg
xmlns="http://www.w3.org/2000/svg"
width="438.549"
height="438.549"
viewBox="0 0 438.549 438.549"
role="img"
aria-labelledby="a9umsuhtdvycvrsulea35mewfzunyupy"
class="icon-img">
<title id="a9umsuhtdvycvrsulea35mewfzunyupy">GitHub logo</title>
<path
d="M409.132
114.573c-19.608-33.596-46.205-60.194-79.798-79.8C295.736 15.166
259.057 5.365 219.27 5.365c-39.78 0-76.47 9.804-110.062
29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0
184.853 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164
63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.42-1.996
2.474-2.282 3.71-5.14 3.71-8.562
0-.57-.05-5.708-.144-15.417-.098-9.71-.144-18.18-.144-25.406l-6.567
1.136c-4.187.767-9.47 1.092-15.846
1-6.375-.09-12.992-.757-19.843-2-6.854-1.23-13.23-4.085-19.13-8.558-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.9-9.233-8.992-14.56-4.093-5.33-8.232-8.944-12.42-10.847l-1.998-1.43c-1.332-.952-2.568-2.1-3.71-3.43-1.143-1.33-1.998-2.663-2.57-3.997-.57-1.335-.097-2.43
1.428-3.29 1.525-.858 4.28-1.275 8.28-1.275l5.708.853c3.807.763
8.516 3.042 14.133 6.85 5.615 3.807 10.23 8.755 13.847 14.843 4.38
7.807 9.657 13.755 15.846 17.848 6.184 4.093 12.42 6.136 18.7
6.136 6.28 0 11.703-.476 16.273-1.423 4.565-.95 8.848-2.382
12.847-4.284 1.713-12.758 6.377-22.56
13.988-29.41-10.847-1.14-20.6-2.857-29.263-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.98-3.9-12.373-5.852-26.647-5.852-42.825
0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.38-36.732
1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.284 18.794
7.953 23.84 10.995 5.046 3.04 9.09 5.618 12.135 7.708 17.706-4.947
35.977-7.42 54.82-7.42s37.116 2.473 54.822
7.42l10.85-6.85c7.418-4.57 16.18-8.757 26.26-12.564 10.09-3.806
17.803-4.854 23.135-3.14 8.562 21.51 9.325 40.923 2.28 58.24
15.035 16.18 22.558 35.788 22.558 58.818 0 16.178-1.958
30.497-5.853 42.966-3.9 12.47-8.94 22.457-15.125 29.98-6.19
7.52-13.9 13.85-23.13 18.985-9.233 5.14-18.183 8.85-26.84
11.135-8.663 2.286-18.416 4.004-29.264 5.146 9.894 8.563 14.842
22.078 14.842 40.54v60.237c0 3.422 1.19 6.28 3.572 8.562 2.38
2.278 6.136 2.95 11.276 1.994 44.163-14.653 80.185-41.062
108.068-79.226 27.88-38.16 41.826-81.126
41.826-128.906-.01-39.77-9.818-76.454-29.414-110.05z" />
</svg>
</a>
<a href="https://dev.to/jpblancodb">
<img
class="icon-img"
src="https://d2fltix0v2e0sb.cloudfront.net/dev-badge.svg"
alt="JPBlancoDB's DEV Profile" />
</a>
</div>
</div>
</div>
</header>
We need to create a Header.svelte
component in src/components
and then we should import it into our src/routes/_layout.svelte
<script>
import Header from "../components/Header.svelte";
</script>
<style>
main {
position: relative;
max-width: 56em;
background-color: white;
padding: 2em;
margin: 0 auto;
box-sizing: border-box;
}
</style>
<svelte:head>
<title>Juan Pablo Blanco - Blog</title>
</svelte:head>
<Header />
<main>
<slot />
</main>
Bonus: Error template
As bonus track (and optional), when any error occurs let's show our Borat image with the error message:
//src/routes/_error.svelte
<script>
export let status;
export let error;
const dev = process.env.NODE_ENV === "development";
</script>
<style>
h1 {
font-size: 2.8em;
font-weight: 700;
margin: 0 0 0.5em 0;
}
@media (min-width: 480px) {
h1 {
font-size: 4em;
}
}
</style>
<h1>{status}</h1>
<figure>
<img alt="Borat" src="great-success.png" />
<figcaption>{error.message}</figcaption>
</figure>
{#if dev && error.stack}
<pre>{error.stack}</pre>
{/if}
For checking this blog running, you could visit my blog: JPBlancoDB
If you have any question, you could leave a comment or ask me via Twitter! I'm happy to help.
ps: I'm using [now](https://zeit.co] and for reference on deploying with v2 Sapper Now v2 demo
Top comments (10)
Interesting, though what I liked the most is the definition of the layout first and the logic is presented later, I'm new to this wizard stuff called software development. Thanks.
PS: Please, don't forget to describe where the Header.svelte file goes and where is has to be described to be present in the page. (_layout.svelte)
Hi Carlos! You are right, I will add it to the article, thank you for reading and the review! πͺ
Really good article! But I have something to add. According to this doc it is not good idea to place a block tags into inline. I suppose it can be important for people who are new to web development and do not know what is good and what is bad practice.
Hi Igor! Thanks for reading! Regarding your comment, Iβm not following which part of this example is a bad practice.
The
<span>
is inline by default, actually is also given as an example in that doc.I would appreciate if you could tell me exactly which part are you referring to.
Thank you!
Cool!!
Thank you Ben!! Cool is dev.to ππ
The dev.to API seems really interesting, thanks for this article. π
Indeed! Thanks for reading!
Why did you moved your page to Zeit? Do you still like Svelte?
Hi! I like Svelte, I still found a little bit challenging doing TDD with it as for example you cannot stub the slots (the PR is still open as far as I know) and found complex to mock other components, but I still think is great coding experience, easy to learn and start with it, definitely give it a try and tell me your experience! Regarding zeit, I've just deployed it there because it is an amazing platform, easy to deploy and has a free tier that works great also for this kind of example. Thanks for reading!