DEV Community

Cover image for Embed dev.to posts into your own website
Nuno Góis
Nuno Góis

Posted on • Updated on • Originally published at nunogois.com

Embed dev.to posts into your own website

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!

Discussion (17)

Collapse
psypher1 profile image
James 'Dante' Midzi

Can you tell me where you got body_html?

I am trying to do the same with my Astro project

Collapse
nunogois profile image
Nuno Góis Author

Sure, when you fetch a single article from the slug, like this: dev.to/api/articles/nunogois/hello... - You'll see that the object it returns also includes this body_html property.

I believe in Astro you can then use set:html={body_html} in the element you wish to add the HTML. Let me know how it works and feel free to share the final result afterwards! I also want to try out Astro soon :)

Collapse
psypher1 profile image
James 'Dante' Midzi

The specific endpoint, I see...

From my testing, I have found this:
This endpoint: https://dev.to/api/articles?username=psypher1 gets my articles but without the body_html

That for some reason is only on the specific paths when you know the path you're trying to get, which presents issues with the way Astro fetches things (needs to know all the paths).... I would need an endpoint that exposes all the post information

Thread Thread
psypher1 profile image
James 'Dante' Midzi

Also, looking deeper at your code, the key part is page.params.slug

I'll work on reverse engineering that for my getStaticPaths

Thread Thread
nunogois profile image
Nuno Góis Author

Hmm, if that's the case I guess you can always fetch everything at once (loop through the articles and fetch their respective information) - That way you would end up with the complete dataset at the start/build time.

You can also check the API docs - Maybe there's a solution for your specific use case where the article info is expanded.

Like I said I'm not familiar with Astro yet, but it feels like it should have a solution for this specific use-case. These resources might help:

Thread Thread
psypher1 profile image
James 'Dante' Midzi

Thank you for this...

It's not the dynamic pages that are the issue - that I can do...

The url I need is one with all posts with all the content... I'll have a dig around the docs

Thread Thread
psypher1 profile image
James 'Dante' Midzi

Hey @nunogois

I got it! The docs were the key... for Astro I have to use this one https://dev.to/api/articles/me/published with an API key

I can write my article now

Thread Thread
nunogois profile image
Nuno Góis Author

Great that you got it, thanks for sharing! 🙂

Thread Thread
psypher1 profile image
James 'Dante' Midzi

Thank you for the help

Collapse
nunogois profile image
Nuno Góis Author

I've since updated my website to add some more CSS rules to properly render this new post - Things like syntax highlighting and the Twitter embed.
This is how this post looks like there: nunogois.com/blog/embed-devto-post...

Next step: Figuring out how to add a webhook to automatically trigger a website build whenever I publish a new post!

Collapse
steinbring profile image
Joe Steinbring

Very cool! I am doing something like that with my website but since I'm going for more of an "activity feed" type of thing, I'm stopping at the URL, title, and image. If you trust dev enough, there really isn't any reason why you can't go a lot further.

Collapse
ahmad_butt_faa7e5cc876ea7 profile image
Ahmad

very cool and creative!! 🤙 could even be a react component which fetches data from a list of blogging websites , and outputs their bodies extracted bodies from the full json.

Collapse
andrewbaisden profile image
Andrew Baisden

Awesome thanks for sharing this!

Collapse
jarvisscript profile image
Chris Jarvis

Very useful blog. Also congrats on the Lighthouse score.

Collapse
psypher1 profile image
James 'Dante' Midzi

I have been trying ro figure out how to this on my site with no luck. This is very helpful thank you

Collapse
ekqt profile image
Hector Sosa

Great post, Nuno. I'm doing something along the same lines. My entire website blog is ran out of Forem APIs. hectorsosa.me/blog