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())
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} />
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}
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}`}...
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>
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;
}
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')
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>`
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>`
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!
Top comments (16)
Can you tell me where you got
body_html
?I am trying to do the same with my Astro project
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 :)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 thebody_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
Also, looking deeper at your code, the key part is
page.params.slug
I'll work on reverse engineering that for my
getStaticPaths
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:
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
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 keyI can write my article now
Great that you got it, thanks for sharing! 🙂
Thank you for the help
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!
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.
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.
Awesome thanks for sharing this!
Very useful blog. Also congrats on the Lighthouse score.
I have been trying ro figure out how to this on my site with no luck. This is very helpful thank you
Great post, Nuno. I'm doing something along the same lines. My entire website blog is ran out of Forem APIs. hectorsosa.me/blog