Embed dev.to posts into your own website
Tags: tutorial, svelte, webdev, opensource
Published: 2022-06-19 17:21:05

I very recently wrote my very [first post](https://dev.to/nunogois/hello-world-4pdf) 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](https://github.com/nunogois/nunogois-website/commit/9fd36ea11ee0f77b8383800cf4d0b190845aa2d3) 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](https://developers.forem.com/api). I used the [getLatestArticles](https://developers.forem.com/api#operation/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](https://kit.svelte.dev/docs/routing#endpoints), one of my favorite features of SvelteKit, which you can see in `src/routes/api.ts`:

```JS
// ...
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())
```

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

```HTML
<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} />
```

## 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`:

```HTML
{#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}
```

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: 

```HTML
<a sveltekit:prefetch href={`/blog/${slug}`}...
```

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:

```HTML
<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>
```

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:

```CSS
.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;
}
```

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`:

```JS
fetch('/sitemap.xml')
fetch('/rss.xml')
```

The source files look like the following:

### sitemap.xml

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

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

```JS
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>`
```

### rss.xml

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

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

```JS
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>`
```

## Conclusion

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

{% embed https://twitter.com/nunogois_dev/status/1537196998545260549 %}

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:

 - https://kit.svelte.dev/docs/introduction
 - https://tailwindcss.com/docs/utility-first

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