loading...

Create a blog with Svelte and DEV.to API

jpblancodb profile image JPBlancoDB Updated on 惻12 min read

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:

Article wireframe

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.

Article template

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

Posted on by:

jpblancodb profile

JPBlancoDB

@jpblancodb

Developer, writing tech articles. Football fan and terrible defender. He/him.

Discussion

markdown guide
 

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!

 
 

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!