Create a blog with Svelte and DEV.to API

jpblancodb

JPBlancoDB

Posted on November 24, 2019

Create a blog with Svelte and DEV.to API

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
  }
]
Enter fullscreen mode Exit fullscreen mode

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}

Enter fullscreen mode Exit fullscreen mode

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;
  }
}

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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}

Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
jpblancodb
JPBlancoDB

Posted on November 24, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related