Nuno Góis

Embed dev.to posts into your own website

Embed dev.to posts into your own website

Jun 19 #tutorial #svelte #webdev #opensource

I very recently wrote my very first post on dev.to - DEV Community, and there I mentioned integrating my dev.to posts into my own website. So here goes!

In case you haven't checked it out already, my website is built with SvelteKit and Tailwind CSS, and it is fully open-source here: https://github.com/nunogois/nunogois-website

You can check the main commit for this feature here but I'll try to break down the important parts below.

API

First of all, we need to fetch the posts, which is very easy to do using the API. I used the getLatestArticles endpoint, which returns "published articles sorted by publish date".

In my case, this what it looks like:
GET https://dev.to/api/articles/latest?username=nunogois - You can test it by accessing this URL in your browser.
Pagination is something that I will later need to think about.

Anyways, in order to integrate this with my website, I leveraged SvelteKit's endpoints, one of my favorite features of SvelteKit, which you can see in src/routes/api.ts:

// ...
export async function get(): Promise<EndpointOutput> {
    return {
        body: {
            // ...
            blog: await loadBlog()
        }
    }
}

export const loadBlog = async (): Promise<JSONString[]> =>
    await fetch('https://dev.to/api/articles/latest?username=nunogois').then((res) => res.json())
Enter fullscreen mode Exit fullscreen mode

This endpoint then gets fetched in my index.svelte file, that passes the blog array as a prop to my Blog component:

<script context="module">
    export async function load({ fetch }) {
        // ...
        const res = await fetch('/api')

        if (res.ok) {
            return {
                props: await res.json()
            }
        }
    }
</script>

<script lang="ts">
    //...
    export let blog
</script>

<Blog {blog} />
Enter fullscreen mode Exit fullscreen mode

Blog

My Blog component is nothing more than a section of my single-page website. The relevant part here is to iterate and render something for each of the blog posts, which you can see in src/pages/blog.svelte:

{#each filteredBlog as { slug, title, description, readable_publish_date, cover_image, tag_list, positive_reactions_count, comments_count, reading_time_minutes }}
  <div class="border border-light-gray rounded-xl">
    <a sveltekit:prefetch href={`/blog/${slug}`} class="flex flex-col h-full">
      <img src={cover_image} alt={title} class="w-full rounded-t-xl object-cover" />
      <h4 class="flex justify-center items-center text-lg font-medium p-2 border-light-gray">
        {title}
      </h4>
      <span class="text-xs text-gray-300 mb-1"
        >{readable_publish_date} - {reading_time_minutes} min read</span
      >
      <span class="text-xs text-gray-300">{tag_list.map((tag) => `#${tag}`).join(' ')}</span>
      <div class="text-xs my-3 mx-5 text-justify">
        {description}
      </div>
      <div class="flex-1 grid grid-cols-2 text-sm content-end">
        <div class="flex justify-center items-center border-t border-light-gray border-r p-1">
          <Icon icon="fa:heart" width="16px" class="inline-block mr-1" />
          {positive_reactions_count}
        </div>
        <div class="flex justify-center items-center border-t border-light-gray border-r p-1">
          <Icon icon="fa:comment" width="16px" class="inline-block mr-1" />
          {comments_count}
        </div>
      </div>
    </a>
  </div>
{/each}
Enter fullscreen mode Exit fullscreen mode

This is currently a bit of a mess, with all of the Tailwind CSS classes and small adjustments, but it looks exactly how I want it for now. Should probably refactor it into its own component soon (BlogItem or something similar).

Now that we have all of the blog posts being displayed, we need a way of opening and reading them. Notice the anchor tag above:

<a sveltekit:prefetch href={`/blog/${slug}`}...
Enter fullscreen mode Exit fullscreen mode

The slug is what uniquely identifies the blog post.

Slug

Leveraging more of SvelteKit's cool features, I created a new src/routes/blog/[slug].svelte file:

<script context="module" lang="ts">
    // ...

    import Icon from '@iconify/svelte'

    export async function load({ page, fetch }) {
        const url = `https://dev.to/api/articles/nunogois/${page.params.slug}`
        const response = await fetch(url)

        return {
            status: response.status,
            props: {
                post: response.ok && (await response.json())
            }
        }
    }
</script>

<script lang="ts">
    export let post
</script>

<div class="flex justify-center">
    <div class="flex flex-col w-full px-4 md:px-24 max-w-screen-lg text-justify pt-16">
        <div class="border-b border-light-gray md:border md:rounded-xl">
            <img src={post.cover_image} alt={post.title} class="w-full rounded-t-xl object-cover mb-4" />
            <div class="md:px-4">
                <div class="flex">
                    <h3 class="w-full text-left text-2xl md:text-3xl font-medium">
                        {post.title}
                    </h3>
                    <a href={post.url} class="w-8"
                        ><Icon icon="fa-brands:dev" width="32px" class="inline-block" /></a
                    >
                </div>
                <div class="flex flex-col pt-2 pb-6 gap-1 text-xs text-gray-300">
                    <span>{post.readable_publish_date}</span>
                    <span>{post.tags.map((tag) => `#${tag}`).join(' ')}</span>
                </div>
                <div class="blog-post">
                    {@html post.body_html}
                </div>
            </div>
        </div>
        <a href={post.url} class="mt-5 text-center">React to this blog post on DEV Community 👩‍💻👨‍💻</a>
        <a href="/" class="my-5 text-center text-sm">www.nunogois.com</a>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

This gets the slug from the URL and uses it to fetch the respective article endpoint, passing it to the props. After that, we just need to render the post however we want.

CSS

Here's the specific CSS I added so far in src/app.css to correctly display the blog post and its embedded content:

.blog-post p {
  margin-bottom: 20px;
}

.blog-post > .crayons-card {
  border-width: 1px;
  --tw-border-opacity: 1;
  border-color: rgb(51 51 51 / var(--tw-border-opacity));
  border-radius: 0.75rem;
  margin-bottom: 20px;
}

.blog-post > .crayons-card > .c-embed__cover img {
  object-fit: cover;
  max-height: 200px;
  border-top-left-radius: 0.75rem;
  border-top-right-radius: 0.75rem;
}

.blog-post > .crayons-card > .c-embed__body {
  padding: 20px;
}

.blog-post > .crayons-card > .c-embed__body > h2 {
  margin-bottom: 8px;
  color: #93ceff;
}

.blog-post > .crayons-card > .c-embed__body > .truncate-at-3 {
  font-size: 0.875rem;
  margin-bottom: 8px;
}

.blog-post > .crayons-card > .c-embed__body > .color-secondary {
  font-size: 0.875rem;
}

.blog-post > .crayons-card .c-embed__favicon {
  max-height: 18px;
  width: auto;
  margin-right: 14px;
}
Enter fullscreen mode Exit fullscreen mode

You can see how this looks like here: https://www.nunogois.com/blog/hello-world-4pdf

Looking pretty nice, if I do say so myself!

Dynamic sitemap.xml and rss.xml

For a bonus round, let's setup a dynamic sitemap.xml and rss.xml.

Note: Here I had to reference their endpoints in the code somehow for them to show up after deployed, which is why I'm fetching them in index.svelte:

fetch('/sitemap.xml')
fetch('/rss.xml')
Enter fullscreen mode Exit fullscreen mode

The source files look like the following:

sitemap.xml

https://www.nunogois.com/sitemap.xml

Here's src/routes/sitemap.xml.ts:

import { loadBlog } from './api'

const website = 'https://www.nunogois.com'

export async function get(): Promise<unknown> {
    const posts = await loadBlog()
    const body = sitemap(posts)

    const headers = {
        'Cache-Control': 'max-age=0, s-maxage=3600',
        'Content-Type': 'application/xml'
    }
    return {
        headers,
        body
    }
}

const sitemap = (posts) => `<?xml version="1.0" encoding="UTF-8" ?>
<urlset
  xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"
  xmlns:news="https://www.google.com/schemas/sitemap-news/0.9"
  xmlns:xhtml="https://www.w3.org/1999/xhtml"
  xmlns:mobile="https://www.google.com/schemas/sitemap-mobile/1.0"
  xmlns:image="https://www.google.com/schemas/sitemap-image/1.1"
  xmlns:video="https://www.google.com/schemas/sitemap-video/1.1"
>
  <url>
    <loc>${website}</loc>
    <changefreq>daily</changefreq>
    <priority>0.7</priority>
  </url>
  ${posts
        .map(
            (post) => `
  <url>
    <loc>${website}/blog/${post.slug}</loc>
    <changefreq>daily</changefreq>
    <priority>0.7</priority>
  </url>
  `
        )
        .join('')}
</urlset>`
Enter fullscreen mode Exit fullscreen mode

rss.xml

https://www.nunogois.com/rss.xml

And here's src/routes/rss.xml.ts:

import { loadBlog } from './api'

const website = 'https://www.nunogois.com'

export async function get(): Promise<unknown> {
    const posts = await loadBlog()
    const body = xml(posts)

    const headers = {
        'Cache-Control': 'max-age=0, s-maxage=3600',
        'Content-Type': 'application/xml'
    }
    return {
        headers,
        body
    }
}

const xml = (
    posts
) => `<rss xmlns:dc="https://purl.org/dc/elements/1.1/" xmlns:content="https://purl.org/rss/1.0/modules/content/" xmlns:atom="https://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title>Nuno Góis - Full-Stack Developer</title>
    <link>${website}</link>
    <description>Full-Stack Developer from Portugal. Experienced with every step of developing and delivering software projects using .NET C#, JavaScript, Go, Python, and more.</description>
    ${posts
            .map(
                (post) =>
                    `
        <item>
          <title>${post.title}</title>
          <description>${post.description}</description>
          <link>${website}/blog/${post.slug}/</link>
          <pubDate>${new Date(post.published_timestamp)}</pubDate>
          <content:encoded>${post.description} 
            <br />
            <a href="${website}/blog/${post.slug}">
              Read more
            </a>
          </content:encoded>
        </item>
      `
            )
            .join('')}
  </channel>
</rss>`
Enter fullscreen mode Exit fullscreen mode

Conclusion

On the way I also made a few corrections and optimizations, which resulted in this Lighthouse score:

This integration is definitely not finished, and I'm pretty sure I'll have to do some extra work right after publishing this post, in order to display it correctly. Still, it was a pretty fun and easy thing to do.

I should probably also take some time to refactor and clean up my website code a bit (and have proper types everywhere), so stay tuned for that.

Feel free to make your own website based on mine, or take some inspiration. If you do, I suggest taking a look at these docs:

Also, please share it, I would love to check it out!

React to this blog post on DEV Community 👩‍💻👨‍💻 www.nunogois.com