Skip to content

Commit 1302b52

Browse files
committed
Restore old articles, but as archived ones, invisible in the lists and rss feeds
1 parent 14d3c40 commit 1302b52

13 files changed

+598
-13
lines changed

Package.resolved

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/Loopwerk/run.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ struct ArticleMetadata: Metadata {
88
let tags: [String]
99
let summary: String?
1010
var heroImage: String?
11+
var archive: Bool?
1112
}
1213

1314
struct AppMetadata: Metadata {
@@ -46,6 +47,12 @@ extension Item where M == ProjectMetadata {
4647
}
4748
}
4849

50+
extension Item where M == ArticleMetadata {
51+
var archive: Bool {
52+
return metadata.archive ?? false
53+
}
54+
}
55+
4956
enum SiteMetadata {
5057
static let url = URL(string: "https://www.loopwerk.io")!
5158
static let name = "Loopwerk"
@@ -87,6 +94,8 @@ struct Run {
8794
metadata: ArticleMetadata.self,
8895
readers: [.parsleyMarkdownReader],
8996
itemProcessor: sequence(improveHTML, publicationDateInFilename, permalink, heroImage),
97+
filter: { $0.archive == false },
98+
filteredOutItemsAreHandled: false,
9099
writers: [
91100
.itemWriter(swim(renderArticle)),
92101
.listWriter(swim(renderArticles)),
@@ -98,6 +107,16 @@ struct Run {
98107
.tagWriter(atomFeed(title: SiteMetadata.name, author: SiteMetadata.author, baseURL: SiteMetadata.url, summary: \.self.metadata.summary), output: "tag/[key]/feed.xml", tags: \.metadata.tags),
99108
]
100109
)
110+
.register(
111+
folder: "articles",
112+
metadata: ArticleMetadata.self,
113+
readers: [.parsleyMarkdownReader],
114+
itemProcessor: sequence(improveHTML, publicationDateInFilename, permalink, heroImage),
115+
filter: { $0.archive == true },
116+
writers: [
117+
.itemWriter(swim(renderArticle)),
118+
]
119+
)
101120
.register(
102121
folder: "apps",
103122
metadata: AppMetadata.self,

Sources/Loopwerk/templates/RenderArticle.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,18 @@ func renderArticleInfo(_ article: Item<ArticleMetadata>) -> Node {
2020
article.date.formatted("MMMM dd, yyyy")
2121
}
2222

23-
%.text("\(article.body.withoutHtmlTags.numberOfWords) words, posted in ")
23+
%.text("\(article.body.withoutHtmlTags.numberOfWords) words")
24+
25+
if article.archive {
26+
%.text(", previously posted in ")
27+
} else {
28+
%.text(", posted in ")
29+
}
2430

2531
article.metadata.tags.sorted().enumerated().map { index, tag in
2632
Node.fragment([
2733
%tagPrefix(index: index, totalTags: article.metadata.tags.count),
28-
%a(href: "/articles/tag/\(tag.slugified)/") { tag },
34+
%a(href: "/articles/tag/\(tag.slugified)/") { tag },
2935
])
3036
}
3137
}
@@ -50,7 +56,8 @@ func getArticleHeader(_ article: Item<ArticleMetadata>) -> NodeConvertible {
5056

5157
func renderArticle(context: ItemRenderingContext<ArticleMetadata>) -> Node {
5258
let extraHeader = getArticleHeader(context.item)
53-
let otherArticles = context.items.filter { $0.url != context.item.url }
59+
let articles = context.allItems.compactMap { $0 as? Item<ArticleMetadata> }.filter { $0.archive == false }
60+
let otherArticles = articles.filter { $0.archive == false && $0.url != context.item.url }
5461
let latestArticles = otherArticles.prefix(2)
5562
let tags = Set(context.item.metadata.tags)
5663

@@ -79,6 +86,10 @@ func renderArticle(context: ItemRenderingContext<ArticleMetadata>) -> Node {
7986
renderArticleInfo(context.item)
8087
}
8188

89+
if context.item.archive {
90+
p(class: "text-gray text-lg font-bold") { "Attention: this is an archived article, and shouldn't be used as a source of information. It's here to preserve the history of this site, to stop link rot, and to allow readers to find out more about the author's work in other contexts." }
91+
}
92+
8293
if let heroImage = context.item.metadata.heroImage {
8394
img(class:"hero-image", src: "/articles/heroes/\(heroImage)")
8495
}

Sources/Loopwerk/templates/RenderArticles.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import Saga
55
func uniqueTagsWithCount(_ articles: [Item<ArticleMetadata>]) -> [(String, Int)] {
66
let tags = articles.flatMap { $0.metadata.tags }
77
let tagsWithCounts = tags.reduce(into: [:]) { $0[$1, default: 0] += 1 }
8-
return tagsWithCounts.sorted { $0.1 > $1.1 }
8+
return tagsWithCounts.sorted {
9+
// Sort by number of articles (descending). If that's the same, sort by title (ascending).
10+
if $0.1 == $1.1 {
11+
return $0.0 < $1.0
12+
}
13+
return $0.1 > $1.1
14+
}
915
}
1016

1117
func renderArticleForGrid(article: Item<ArticleMetadata>) -> Node {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
tags: review
3+
archive: true
4+
---
5+
6+
# Switching from MobileMe & my short adventure with Google App Engine
7+
I can't remember how long ago it's been that I've used a free email address like hotmail, yahoo or gmail. For more than 8 years I've used my own domain names to host my email. In the very beginning on my own little server at home, later on a "real" colocated server when I still had my own company, on the servers of my employer and then last year on Apple's [MobileMe](http://www.apple.com/mobileme/) service.
8+
9+
Last September, my MobileMe subscription needed to be renewed, and it got me thinking if that was something I really wanted to do. It's $99 a year for 20 GB of email/calendar/contacts hosting and iDisk for online storage. It also offers easy web galleries with a click of a button within iPhoto and great syncing between my home- and work Macs and my iPhone. Not too pricy for what you get really, but still... it got me thinking.
10+
11+
Last September I also got a new job, where I no longer work on an Apple computer, but on a laptop running Ubuntu. The first problem with MobileMe quickly popped up: it doesn't sync your calendars or contacts to Linux. So on the "plus" side of MobileMe I could erase its ease of syncing everything.
12+
13+
After searching for good alternatives to MobileMe I found the following solution: Google Apps and Flickr. [Google Apps](http://www.google.com/apps/intl/en/business/index.html) takes care of my email (on my own domain name), calendar and contacts, can sync it between my Mac (Mail.app, iCal and Address Book) and my Linux PC (Thunderbird and Sunbird) and with their Exchange support, all changes get pushed to my iPhone immediately.
14+
15+
That left the question of photo hosting, something that is very important to me. I went with [Flickr](http://www.flickr.com/), and got myself a Pro account for $25 a year. It offers unlimited photo hosting with great iPhoto support. The free [Dropbox](https://www.getdropbox.com/referrals/NTIzMTA5NjU5) software is my iDisk replacement that comes with 2 GB of online storage, and is easier and faster to use. The only thing I can't replicate is the "Find My iPhone" service which MobileMe provided, but for the $74/year that I now save, I can live without that just fine. Drawback so far? Well, the Flickr UI is ten times worse than the beautiful MobileMe web galleries, but that's where the Flickr API comes in - you can make your own UI as I did on this site.
16+
17+
Other free software I use to sync everything are [Xmarks](http://www.xmarks.com/) to sync my bookmarks between my home Mac and work PC, and [LastPass](https://lastpass.com/), to sync my site passwords between all browsers at home and at work.
18+
19+
When I started the new bolhoed.net in Django I of course needed hosting. The previous version was just one simple static html page that I hosted on my employer's server, but with a new dynamic site, I thought it might be better to try [Google App Engine](http://code.google.com/appengine/) (GAE). It sounds really really great: free hosting of Python or Java projects, about 1.3 million requests per day, 1 GB of outgoing traffic per day and 1 GB of storage. All for free! And with presumably good Django support! So I started rewriting the bits of code that would not work out of the box on GAE, and indeed got it running on their servers within two hours.
20+
21+
Once it was running, I started thinking about making a blog to share my wonderful experience with Google products. And sadly, that's where the cracks started to appear. I now needed a database to store the blog posts and comments - and since Google only offers their own object oriented Data Store, the Django ORM was completely useless. Gone are all of the handy shortcut functions, views and even admin site. I did find the [App Engine Patch](http://code.google.com/p/app-engine-patch/) to be useful (it simulates enough of the Django ORM to re-enable the admin site for example), but there were problems. The local SDK doesn't work correctly with Python 2.6, the Data Store uses ridiculous keys so you get very long and ugly urls and the standard "mail on sever error" functionality doesn't work. So, I re-rebuilt the code to use all normal Django functions on a normal MySQL database once again and finally hosted it on the servers of Goldmund, Wyldebeast & Wunderliebe. Again.
22+
23+
I think that when you are building a Django project that does not need a database, then Google App Engine is the greatest thing since sliced bread. Once you need a database however, too much of the Django magic is gone and leaves you feeling a bit naked. Maybe a framework like Pylons would be better suited, I don't know.
24+
25+
One last thing: I really do think Google Apps is a great thing to host your email, calendar and contacts on. Not just me, but Goldmund, Wyldebeast & Wunderliebe uses it for all of their business email and calendar. We run our company infrastructure (so to speak) on Google, and it doesn't cost a thing. Compare that to maintaining your own servers, and I think we got a clear winner. Add Google App Engine in the mix, and you could run your entire company including hosting your websites, for free, without needing a server admin at all. Pretty neat!
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
tags: review, python, django
3+
archive: true
4+
---
5+
6+
# Django 1.2, a great release
7+
I've been playing with the Beta release of Django 1.2 ([get it here](http://www.djangoproject.com/download/)) and I love many of the new improvements. I'd like to list the best and biggest new features, and also some problems I encountered while using it.
8+
9+
## Smarter if tag
10+
11+
I was already using [this snippet](http://www.djangosnippets.org/snippets/1350/) a lot, and that is no longer necessary. This is now in Django's core, making it so much easier to use if statements in html templates.
12+
13+
## Multiple database support
14+
15+
Personally I don't expect I will use this a lot, but it's great to see that it's possible with a standard Django configuration. Read more about it [on the official documentation](http://docs.djangoproject.com/en/dev/topics/db/multi-db/).
16+
17+
## Cached template loaders
18+
19+
Before Django 1.2, all templates needed to be parsed from disc for every request. This can be a (small) problem if you have lots of nested templates. I am very happy with this change, but have yet to see its impact. As a side-effect of the new template loaders, it is now much much easier to use different template languages like Jinja2, which I blogged about in a [previous post](/articles/2009/using-jinja/).
20+
21+
## Custom email backends
22+
23+
While I am generally happy to use the basic smtp backend for email, it is good to see that this change will allow Google App Engine users to use Django's email functions again. Now only proper database support is missing, but then it will be very easy to switch to Google App Engine indeed.
24+
25+
## New message system
26+
27+
The old message system was flawed. It didn't support different message levels (notice, warning, succes, error, etc), was only available for logged in users, and required an extra database hit on every request. The new generic message system solves all of these problems and I couldn't be happier.
28+
29+
## I18N/L10N improvements
30+
31+
Django 1.2 can show dates and numbers in the right way. For example, in The Netherlands, we show dates as 13-02-1982, while in England they use 02/13/1982. Also big numbers with separators for groups of thousands are now locale aware. Since I am currently working on an application with support for English, Dutch and German, this is a very very welcome improvement to Django.
32+
33+
The only problems I had with getting this to work was finding out which setting to change, because this wasn't documented yet, or at the very least hard to find. If you are interested in the new L10N improvements, add this to your settings.py:
34+
35+
```python
36+
USE_I18N = True
37+
USE_L10N = True
38+
USE_THOUSAND_SEPARATOR = True
39+
```
40+
41+
Of course there are many more improvements like better tab completion in bash, object permissions, object validation, better CSRF protection and a better test framework. But those are features not yet used by me, although I am very interested in object validation.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
tags: python, django
3+
archive: true
4+
---
5+
6+
# Serving 5000 pages per second with Django
7+
Okay, this website wasn't slow and will never need to serve 5000 pages per second, but hey, it's possible now! Oh right, and it was fun to play around with a nice caching system.
8+
9+
Almost everything on mixedCase.nl needs to come from the database: the list of categories, the menu on top of the site, the articles, the count of the number of comments, how many entries there are per month and category, and so on. So, caching makes a big difference in making the site very quick. When I wanted to play around with caching, I already knew about these options Django provides out of the box:
10+
11+
* Cache individual views with a decorator
12+
* Cache pieces of the rendered templates with the cache templatetag
13+
* Use Django's cache middleware to cache the output of entire pages to memcached
14+
15+
Option 1 was not an option, since all views are provided by third-party applications ([Django-CMS](https://github.com/divio/django-cms) and one of my all-time favorite apps: [Zinnia](https://github.com/Fantomas42/django-blog-zinnia)). Option 2 is better, because all templates are written by myself, so I can do whatever I want. While this is indeed a very nice option if you want to cache "expensive" bits of your pages, Django still has to do some work. The third option is pretty cool: insert two pieces of middleware in your `settings.py` and your entire site is cached. However, the webserver still needs to call your Django instance (in my case [Gunicorn](http://gunicorn.org/)).
16+
17+
Today I was looking at the documentation of [Nginx](http://wiki.nginx.org/Main), the webserver I use. Listed in the available modules: [HttpMemcachedModule](http://wiki.nginx.org/HttpMemcachedModule). Wow, the webserver can use your memcached cache, so it doesn't even have to hit the Django server! After looking into this, I quickly saw a problem: Nginx can use the current url as a cache-key, but not much else. And Django's cache middleware uses a key composed of cookies, sessions, url and md5 hashes. So, the cache as written by Django cannot be read from Nginx. Bummer.
18+
19+
As the saying goes: Google is your friend, and it wasn't long before I found some posts about this very problem. Some people wrote their own cache middleware, so it's readable from Nginx. However, invalidating the cache looked difficult in all their solutions: whenever a new comment is made for example, you need to remove the cache for the article, the list of articles on /articles/, all category lists that article is on, and the homepage. Why? Because they all show a count of the comments made on articles. And since all those pages are saved in memcached with an URL as their key, you need to find all those URL's to remove them from the cache. Ouch.
20+
21+
I then found a very interesting project on Github: [StaticGenerator for Django](https://github.com/luckythetourist/staticgenerator). In short, it's a piece of middleware that saves the output of the requested page as an HTML file, which can be served from Nginx. And since it's just a bunch of files on disk, it's very easy to just remove all of them whenever something on the website has changed (a new page or article has been added, a comment has been made, and so on). I made some modifications on the StaticGenerator, because I only want to cache pages for anonymous users, and want to be able to set a list of excluded URL's. The [source code is available](https://github.com/kevinrenskers/mixedcase-python/tree/master/project/staticgenerator) on my GitHub account.
22+
23+
To use the StaticGenerator, add these settings to `settings.py`:
24+
25+
```python
26+
WEB_ROOT = os.path.join(os.path.dirname(__file__), 'generated')
27+
28+
STATIC_GENERATOR_ANONYMOUS_ONLY = True
29+
30+
STATIC_GENERATOR_URLS = (
31+
r'^/$',
32+
r'^/(articles|projects|about)',
33+
)
34+
35+
STATIC_GENERATOR_EXCLUDE_URLS = (
36+
r'\.xml$',
37+
r'^/articles/search',
38+
r'^/articles/feed',
39+
r'^/articles/comments/posted',
40+
)
41+
```
42+
43+
You also need to add `'staticgenerator.middleware.StaticGeneratorMiddleware'` to the end of your `MIDDLEWARE_CLASSES` list.
44+
45+
Of course, you want to remove the generated pages as soon as something has changed. You can simply add something like this to one of your `models.py` files:
46+
47+
```python
48+
from django.db.models.signals import post_save
49+
from staticgenerator import recursive_delete
50+
51+
def delete_cache(sender, **kwargs):
52+
recursive_delete('/')
53+
54+
post_save.connect(delete_cache)
55+
```
56+
57+
Please note that this is a very simple implementation: every time any of your models is saved, all generated pages are removed. This includes someone simply logging in on the admin site as well, which is of course not something you would want. I'll update this post with a better way of doing this.
58+
59+
Finally, my (shortened) Ngix config:
60+
61+
```nginx
62+
server {
63+
server_name .mixedcase.nl;
64+
root /path/to/project/generated/;
65+
66+
access_log /var/log/nginx/mixedcase.nl.access.log;
67+
68+
location /media/ {
69+
alias /path/to/project/media/;
70+
access_log off;
71+
expires max;
72+
}
73+
74+
location /adminmedia/ {
75+
alias /path/to/project/lib/python2.6/site-packages/django/contrib/admin/media/;
76+
access_log off;
77+
expires max;
78+
}
79+
80+
location / {
81+
if (-f $request_filename/index.html) {
82+
rewrite (.*) $1/index.html break;
83+
}
84+
85+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
86+
proxy_set_header Host $http_host;
87+
proxy_redirect off;
88+
89+
if (!-f $request_filename) {
90+
proxy_pass http://unix:/tmp/gunicorn_mixedcase.sock;
91+
break;
92+
}
93+
}
94+
}
95+
```
96+
97+
As I said in the first sentence: mixedCase.nl can now do 5000 pages per second. I'm impressed!

0 commit comments

Comments
 (0)