Blog Design Approach

Blog landing pages present an interesting challenge for web designers. Like any page, they have to be accessible, impactful and responsive. How we try to achieve those objectives is the subject of this post.

The typical landing page of many blogs resemble the home pages of online newspapers. Usually there is a prominent featured article. When you hover over an article the colour of the text changes and is underlined to appear like a link with a cursor encouraging you to read the full content.

The CMS orders blog items using this SQL:

ORDER BY display_order NULLS FIRST, updated_date DESC

Unless you drag & drop items in the list to re-order them, the underlying content table has  “display_order” set to NULL; in this case items are ordered by latest updated_date instead - i.e. most recently updated first.

If you do drag & drop items in the dropdown list then these will have their display_order set to the order in which you make them appear. Any blog items you add subsequently will have display_order NULL and hence, will display first in the list; multiple blogs with NULL display_order are displayed in descending order of updated_date. 

HTML

A common tactic is to define the blog items as an ordered list - screen readers conveniently announce the list and the number of items it contains.

<ol class="blog">
	<li class="card">
		<img src="https://res.cloudinary.com/mrbapex/q_auto,f_auto,w_256/blog-photo_lgiqim">
		<div class="text">
        	<h2>
            	<a href="#">Blog Design Approach</a>
            </h2>
            <span class="date">28 April 2024</span>
            <p>Blog landing pages present an interesting challenge for web designers...</p>
            <span class="words">20 words</span>
        </div>
    </li>
    <!-- as many <li> as card teasers on the page -->
</ol>

The first item in the list is the featured article - for our blog it will be twice as large as the other cards on the page. 

Elsewhere on this site I said there were no media queries. That was a lie. The only way I was able to make the first card twice as large as the others whilst remaining fully responsive was to change the containing list's display property from grid to flex when the client's viewport is less than 2 card widths, i.e. 2 x 22 x 16.

.blog {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(22rem,100%), 1fr));
    gap: var(--space-m);
	& :nth-child(1) {
    	grid-column: span 2;
   	}
}

/* Unfortunately need this for component to remain responsive when first card spans 2 columns */
@media (max-width: 680px) {
  .blog {
    display: flex;
    flex-direction: column;
  }
  .card {
    margin-bottom: 2em;
  }
}

Individual list items are defined with the “card” class and comprise 2 top-level elements - <img> for any image and <div class="text"> for the title, published date, excerpt and word count.

Card elements are stacked vertically with “display: flex; flex-direction: column;”

To make the entire card clickable we need “position:relative” on the card and “position: absolute; left: 0; right: 0; top: 0; bottom: 0;” on the anchor's pseudo ::after element.



.card {
    --border-radius: var(--space-2xs);
    display: flex;
    flex-direction: column;
    position: relative;
    border: solid 1px var(--color-primary);
    border-radius: var(--border-radius);
    list-style-type: none;
    container-type: inline-size;
    & .text {
        display:flex;
        flex-direction: column;
        gap: var(--space-s);
        flex-grow: 1;
        padding: var(--space-s);
        & :nth-last-child(2) {
            margin-bottom: var(--space-l);
        }
        & > :last-child {
            margin-top: auto;
            font-style: italic;
        }
    }
    & .text > h2 {
        @container (width >= 0) {
            font-size: var(--step-3);
            text-transform: uppercase;
        }
    }
    & .text > span {
        @container (width >= 0) {
            font-size: var(--step--1);
        }
    }
    & .text > p {
        @container (width >= 0) {
            font-size: var(--step-0);
        }
    }
    & img {
        inline-size: 98%;
        block-size: 25vh;
        margin-inline: auto;
        object-fit: cover;
        border-top-left-radius: var(--border-radius);
        border-top-right-radius: var(--border-radius);
    }
    & a {
        text-decoration: none;
        color: var(--color-text);
    }
    & a::after {
        content: " ";
        position: absolute;
        left: 0;
        top: 0;
        right: 0;
        bottom: 0;
    }
    & .date {
        font-family: monospace;
        color: cornflowerblue;
        margin-top: calc(-0.8 * var(--space-s));
    }
    & .words {
        margin-inline-start: auto;
    }
    &:hover, &:focus-within {
        box-shadow: 4.0px 8.0px 8.0px hsl(0deg 0% 0% / 0.38);
    }
    &:hover a {
        color: var(--color-primary);
        text-decoration: underline;
        text-decoration-color: var(--color-primary);
        text-decoration-skip-ink: none;
    }
}