Skip to content

Commit 921dfee

Browse files
authored
Merge pull request #61 from linkdotnet/feature/fallback-image
Feature/fallback image
2 parents d9f1cef + 645685b commit 921dfee

File tree

17 files changed

+217
-32
lines changed

17 files changed

+217
-32
lines changed

Readme.md

+8-8
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,13 @@ To get better results when for example shared via LinkedIn some of the `<meta pr
167167

168168
The following tags are set depending on the page:
169169

170-
| Open Graph Tag | Index | Display Blog Post |
171-
| -------------- | --------------------------------------------------------- | -------------------------------------------- |
172-
| og:title | Title of the blog (defined in Introduction) | Title of the Blog Post |
173-
| og:url | Url to the index page | Url of the page |
174-
| og:image | Profile image (defined in Introduction) | Yes |
175-
| og:type | article | article |
176-
| og:description | Short description in plain text (defined in Introduction) | Short Description of Blog Post in plain text |
170+
| Open Graph Tag | Index | Display Blog Post |
171+
| -------------- | --------------------------------------------------------- |-----------------------------------------------------------------------------|
172+
| og:title | Title of the blog (defined in Introduction) | Title of the Blog Post |
173+
| og:url | Url to the index page | Url of the page |
174+
| og:image | Profile image (defined in Introduction) | Uses the preview image. If a fallback is defined this will be used instead. |
175+
| og:type | article | article |
176+
| og:description | Short description in plain text (defined in Introduction) | Short Description of Blog Post in plain text |
177177

178178
Furthermore the following tags are set:
179179

@@ -183,4 +183,4 @@ Furthermore the following tags are set:
183183
| &lt;meta name="keyword" content="" /&gt; | not set | Tags defined in the Blog Post |
184184

185185
## RSS Feed
186-
This blog also offers a RSS feed ([RSS 2.0 specification](https://validator.w3.org/feed/docs/rss2.html)), which can be consumed by your users or programs like feedly. Just append `feed.rss` to your url or click on the RSS feed icon in the navigation bar to get the feed. The RSS feed does not expose the whole content of a given blog post but it's title and short description including some other tags like preview image, publishing date and so on.
186+
This blog also offers a RSS feed ([RSS 2.0 specification](https://validator.w3.org/feed/docs/rss2.html)), which can be consumed by your users or programs like feedly. Just append `feed.rss` to your url or click on the RSS feed icon in the navigation bar to get the feed. The RSS feed does not expose the whole content of a given blog post but it's title and short description including some other tags like preview image, publishing date and so on.

src/LinkDotNet.Blog.Domain/BlogPost.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ private BlogPost()
1818

1919
public string PreviewImageUrl { get; private set; }
2020

21+
public string PreviewImageUrlFallback { get; private set; }
22+
2123
public DateTime UpdatedDate { get; private set; }
2224

2325
public virtual ICollection<Tag> Tags { get; private set; }
@@ -33,7 +35,8 @@ public static BlogPost Create(
3335
string previewImageUrl,
3436
bool isPublished,
3537
DateTime? updatedDate = null,
36-
IEnumerable<string> tags = null)
38+
IEnumerable<string> tags = null,
39+
string previewImageUrlFallback = null)
3740
{
3841
var blogPost = new BlogPost
3942
{
@@ -42,6 +45,7 @@ public static BlogPost Create(
4245
Content = content,
4346
UpdatedDate = updatedDate ?? DateTime.Now,
4447
PreviewImageUrl = previewImageUrl,
48+
PreviewImageUrlFallback = previewImageUrlFallback,
4549
IsPublished = isPublished,
4650
Tags = tags?.Select(Tag.Create).ToList(),
4751
};
@@ -61,6 +65,7 @@ public void Update(BlogPost from)
6165
Content = from.Content;
6266
UpdatedDate = from.UpdatedDate;
6367
PreviewImageUrl = from.PreviewImageUrl;
68+
PreviewImageUrlFallback = from.PreviewImageUrlFallback;
6469
IsPublished = from.IsPublished;
6570
ReplaceTags(from.Tags);
6671
}

src/LinkDotNet.Blog.Infrastructure/Persistence/Sql/Mapping/BlogPostConfiguration.cs

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public void Configure(EntityTypeBuilder<BlogPost> builder)
1717
builder.Navigation(x => x.Tags).AutoInclude();
1818
builder.Property(x => x.Title).HasMaxLength(256).IsRequired();
1919
builder.Property(x => x.PreviewImageUrl).HasMaxLength(1024).IsRequired();
20+
builder.Property(x => x.PreviewImageUrlFallback).HasMaxLength(1024);
2021
builder.Property(x => x.Content).IsRequired();
2122
builder.Property(x => x.ShortDescription).IsRequired();
2223
builder.Property(x => x.Likes).IsRequired();

src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor

+7
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@
3030
<div class="mb-3">
3131
<label for="preview">Preview-Url</label>
3232
<InputText class="form-control" id="preview" @bind-Value="model.PreviewImageUrl"/>
33+
<small for="preview" class="form-text text-muted">The primary image which will be used.</small>
34+
</div>
35+
<div class="mb-3">
36+
<label for="preview">Fallback Preview-Url</label>
37+
<InputText class="form-control" id="fallback-preview" @bind-Value="model.PreviewImageUrlFallback"/>
38+
<small for="fallback-preview" class="form-text text-muted">Optional: Used as a fallback if the preview image can't be used by the browser.
39+
<br>For example using a jpg or png as fallback for avif which is not supported in Safari or Edge.</small>
3340
</div>
3441
<div class="form-check">
3542
<InputCheckbox class="form-check-input" id="published" @bind-Value="model.IsPublished" />

src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public class CreateNewModel
3131

3232
public DateTime OriginalUpdatedDate { get; set; }
3333

34+
public string PreviewImageUrlFallback { get; set; }
35+
3436
public static CreateNewModel FromBlogPost(BlogPost blogPost)
3537
{
3638
return new CreateNewModel
@@ -43,6 +45,7 @@ public static CreateNewModel FromBlogPost(BlogPost blogPost)
4345
IsPublished = blogPost.IsPublished,
4446
PreviewImageUrl = blogPost.PreviewImageUrl,
4547
OriginalUpdatedDate = blogPost.UpdatedDate,
48+
PreviewImageUrlFallback = blogPost.PreviewImageUrlFallback,
4649
};
4750
}
4851

@@ -53,7 +56,7 @@ public BlogPost ToBlogPost()
5356
? null
5457
: OriginalUpdatedDate;
5558

56-
var blogPost = BlogPost.Create(Title, ShortDescription, Content, PreviewImageUrl, IsPublished, updatedDate, tags);
59+
var blogPost = BlogPost.Create(Title, ShortDescription, Content, PreviewImageUrl, IsPublished, updatedDate, tags, PreviewImageUrlFallback);
5760
blogPost.Id = Id;
5861
return blogPost;
5962
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
@using Microsoft.AspNetCore.StaticFiles
2+
@if (string.IsNullOrEmpty(PreviewImageUrlFallback))
3+
{
4+
<img src="@PreviewImageUrl" alt="Preview image blogpost" loading="@LazyLoadTag"/>
5+
}
6+
else
7+
{
8+
<picture>
9+
<source srcset="@PreviewImageUrl" type="@GetMimeType()"/>
10+
<img src="@PreviewImageUrlFallback" alt="Preview image blogpost" loading="@LazyLoadTag"/>
11+
</picture>
12+
}
13+
14+
@code {
15+
[Parameter]
16+
public string PreviewImageUrl { get; set; }
17+
18+
[Parameter]
19+
public string PreviewImageUrlFallback { get; set; }
20+
21+
[Parameter]
22+
public bool LazyLoadImage { get; set; }
23+
24+
private string LazyLoadTag => LazyLoadImage ? "lazy" : "eager";
25+
26+
private static readonly FileExtensionContentTypeProvider Provider = new();
27+
28+
static PreviewImage()
29+
{
30+
if (!Provider.Mappings.Keys.Contains(".avif"))
31+
{
32+
Provider.Mappings.Add(".avif", "image/avif");
33+
}
34+
}
35+
36+
private string GetMimeType()
37+
{
38+
Provider.TryGetContentType(PreviewImageUrl, out var contentType);
39+
return contentType;
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
img {
2+
position: absolute;
3+
top: 0;
4+
left: 0;
5+
object-fit: cover;
6+
height: 100%;
7+
width: 100%;
8+
}

src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor

+8-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
<article>
66
<div class="blog-card @AltCssClass">
77
<div class="meta">
8-
<div class="photo" style="background-image: url(@BlogPost.PreviewImageUrl)"></div>
8+
<div class="photo">
9+
<PreviewImage PreviewImageUrl="@BlogPost.PreviewImageUrl"
10+
PreviewImageUrlFallback="@BlogPost.PreviewImageUrlFallback"
11+
LazyLoadImage="@LazyLoadPreviewImage"></PreviewImage>
12+
</div>
913
<ul class="details">
1014
<li class="date">@BlogPost.UpdatedDate.ToString("dd/MM/yyyy")</li>
1115
@if (BlogPost.Tags != null)
@@ -39,6 +43,9 @@
3943

4044
[Parameter]
4145
public bool UseAlternativeStyle { get; set; }
46+
47+
[Parameter]
48+
public bool LazyLoadPreviewImage { get; set; }
4249

4350
private string AltCssClass => UseAlternativeStyle ? "alt" : string.Empty;
4451

src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor.css

-13
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,11 @@
2323
.blog-card a:hover {
2424
color: #5ad67d;
2525
}
26-
.blog-card:hover .photo {
27-
transform: scale(1.3) rotate(3deg);
28-
}
2926
.blog-card .meta {
3027
position: relative;
3128
z-index: 0;
3229
height: 200px;
3330
}
34-
.blog-card .photo {
35-
position: absolute;
36-
top: 0;
37-
right: 0;
38-
bottom: 0;
39-
left: 0;
40-
background-size: cover;
41-
background-position: center;
42-
transition: transform 0.5s;
43-
}
4431
.blog-card .details,
4532
.blog-card .details ul {
4633
margin: auto;

src/LinkDotNet.Blog.Web/Features/Home/Index.razor

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<div class="content px-4 my-2">
2828
@for (var i = 0; i < currentPage.Count; i++)
2929
{
30-
<ShortBlogPost BlogPost="currentPage[i]" UseAlternativeStyle="@(i % 2 != 0)"></ShortBlogPost>
30+
<ShortBlogPost BlogPost="currentPage[i]" UseAlternativeStyle="@(i % 2 != 0)" LazyLoadPreviewImage="i > 2"></ShortBlogPost>
3131
}
3232
</div>
3333
<BlogPostNavigation PageList="@currentPage"></BlogPostNavigation>

src/LinkDotNet.Blog.Web/Features/ShowBlogPost/ShowBlogPostPage.razor

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ else
1616
{
1717
<PageTitle>@BlogPost.Title</PageTitle>
1818
<OgData Title="@BlogPost.Title"
19-
AbsolutePreviewImageUrl="@BlogPost.PreviewImageUrl"
19+
AbsolutePreviewImageUrl="@OgDataImage"
2020
Description="@(Markdown.ToPlainText(BlogPost.ShortDescription))"
2121
Keywords="@Tags"></OgData>
2222
<div class="blog-outer-box">
@@ -53,6 +53,8 @@ else
5353
private string Tags => BlogPost?.Tags != null
5454
? string.Join(",", BlogPost.Tags.Select(b => b.Content))
5555
: null;
56+
57+
private string OgDataImage => BlogPost.PreviewImageUrlFallback ?? BlogPost.PreviewImageUrl;
5658

5759
private BlogPost BlogPost { get; set; }
5860

src/LinkDotNet.Blog.Web/Pages/_Layout.cshtml

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@
4141
An unhandled exception has occurred. See browser dev tools for details.
4242
</environment>
4343
<a href="" class="reload">Reload</a>
44-
<a class="dismiss">🗙</a>
44+
<a class="dismiss">x</a>
4545
</div>
4646
<script src="_framework/blazor.server.js"></script>
4747
<script async src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/highlight.min.js" integrity="sha512-yUUc0qWm2rhM7X0EFe82LNnv2moqArj5nro/w1bi05A09hRVeIZbN6jlMoyu0+4I/Bu4Ck/85JQIU82T82M28w==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
48-
<script async src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/languages/csharp.min.js" integrity="sha512-wOhBmvmYJyIcnoY/XHP/c7SyyK05H0NleQgId+c6taC3OWIJNj0DbG0J8dAuxpWNjsk84y4yHQraUtJnWHnpNA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script></script>
48+
<script async src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/languages/csharp.min.js" integrity="sha512-wOhBmvmYJyIcnoY/XHP/c7SyyK05H0NleQgId+c6taC3OWIJNj0DbG0J8dAuxpWNjsk84y4yHQraUtJnWHnpNA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
4949
<script async src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js" integrity="sha256-cMPWkL3FzjuaFSfEYESYmjF25hCIL6mfRSPnW8OVvM4=" crossorigin="anonymous"></script>
5050
<script async src="components/selection.js" ></script>
5151
<script async src="components/slideshow.js" ></script>

tests/LinkDotNet.Blog.TestUtilities/BlogPostBuilder.cs

+10-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ public class BlogPostBuilder
88
private string title = "BlogPost";
99
private string shortDescription = "Some Text";
1010
private string content = "Some Content";
11-
private string url = "localhost";
11+
private string previewImageUrl = "localhost";
12+
private string previewImageUrlFallback = null;
1213
private bool isPublished = true;
1314
private string[] tags;
1415
private int likes;
@@ -34,7 +35,13 @@ public BlogPostBuilder WithContent(string content)
3435

3536
public BlogPostBuilder WithPreviewImageUrl(string url)
3637
{
37-
this.url = url;
38+
previewImageUrl = url;
39+
return this;
40+
}
41+
42+
public BlogPostBuilder WithPreviewImageUrlFallback(string url)
43+
{
44+
previewImageUrlFallback = url;
3845
return this;
3946
}
4047

@@ -64,7 +71,7 @@ public BlogPostBuilder WithUpdatedDate(DateTime updateDate)
6471

6572
public BlogPost Build()
6673
{
67-
var blogPost = BlogPost.Create(title, shortDescription, content, url, isPublished, updateDate, tags);
74+
var blogPost = BlogPost.Create(title, shortDescription, content, previewImageUrl, isPublished, updateDate, tags, previewImageUrlFallback);
6875
blogPost.Likes = likes;
6976
return blogPost;
7077
}

tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public void ShouldUpdateBlogPost()
1212
{
1313
var blogPostToUpdate = new BlogPostBuilder().Build();
1414
blogPostToUpdate.Id = "random-id";
15-
var blogPost = BlogPost.Create("Title", "Desc", "Content", "Url", true);
15+
var blogPost = BlogPost.Create("Title", "Desc", "Content", "Url", true, previewImageUrlFallback: "Url2");
1616
blogPost.Id = "something else";
1717

1818
blogPostToUpdate.Update(blogPost);
@@ -21,6 +21,7 @@ public void ShouldUpdateBlogPost()
2121
blogPostToUpdate.ShortDescription.Should().Be("Desc");
2222
blogPostToUpdate.Content.Should().Be("Content");
2323
blogPostToUpdate.PreviewImageUrl.Should().Be("Url");
24+
blogPostToUpdate.PreviewImageUrlFallback.Should().Be("Url2");
2425
blogPostToUpdate.IsPublished.Should().BeTrue();
2526
blogPostToUpdate.Tags.Should().BeNullOrEmpty();
2627
}

tests/LinkDotNet.Blog.UnitTests/Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPostTests.cs

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public void ShouldCreateNewBlogPostWhenValidDataGiven()
2424
cut.Find("#short").Input("My short Description");
2525
cut.Find("#content").Input("My content");
2626
cut.Find("#preview").Change("My preview url");
27+
cut.Find("#fallback-preview").Change("My fallback preview url");
2728
cut.Find("#published").Change(false);
2829
cut.Find("#tags").Change("Tag1,Tag2,Tag3");
2930

@@ -35,6 +36,7 @@ public void ShouldCreateNewBlogPostWhenValidDataGiven()
3536
blogPost.ShortDescription.Should().Be("My short Description");
3637
blogPost.Content.Should().Be("My content");
3738
blogPost.PreviewImageUrl.Should().Be("My preview url");
39+
blogPost.PreviewImageUrlFallback.Should().Be("My fallback preview url");
3840
blogPost.IsPublished.Should().BeFalse();
3941
blogPost.UpdatedDate.Should().NotBe(default);
4042
blogPost.Tags.Should().HaveCount(3);

0 commit comments

Comments
 (0)