Compare commits

...

2 Commits

Author SHA1 Message Date
Leonora Tindall 3f008be16c Include header and footer, and nginx config. 2022-09-20 11:44:33 -05:00
Leonora Tindall 338e22dc69 Support images and embeds properly. 2022-09-20 11:44:20 -05:00
4 changed files with 129 additions and 27 deletions

View File

@ -1,3 +1,38 @@
# Markdown @Edge
# Markdown@Edge
A Markdown renderer on the Compute @Edge platform from Fastly!
A Markdown renderer on the Compute@Edge platform from Fastly.
The main source file, `src/main.rs`, contains all the code for the renderer.
The header and footer files I use are included at `content/_header.md` and
`content/_footer.md` for easy inspection.
## Nginx Configuration
In order to best support this application, it's important to add the following
line to Nginx's MIME types configuration:
```text
text/markdown md;
```
I also disabled gzip and set the max-age and cache-control headers as follows:
```nginx
server {
# ...
location /edgeblog {
# To illustrate the source to visitors.
autoindex on;
autoindex_exact_size off;
autoindex_format html;
autoindex_localtime on;
# To facilitate tagging.
gzip off;
# Cache-Control headers
expires 1h;
add_header Cache-Control "public";
}
}
```

10
content/_footer.md Normal file
View File

@ -0,0 +1,10 @@
---
<h5>
Created by [Leonora Tindall](./author.md)
| [Code](https://git.nora.codes/nora/edgeblog)
| [Blog](https://nora.codes)
| [Fastly](https://fastly.com)
</h5>

38
content/_header.md Normal file
View File

@ -0,0 +1,38 @@
<style>
body {
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
h1, h2, h3, h4, h5, h6 {
text-align: center;
}
h1 a {
text-decoration: none;
color: black;
font-weight: bold;
}
img {
margin-left: auto;
margin-right: auto;
margin-top: 2px;
margin-bottom: 8px;
aspect-ratio: attr(width) / attr(height);
}
img[alt="A diagram showing the difference in pipeline between normal websites with a CDN and this monstrosity"] {
width: 582px;
height: 521px;
}
</style>
<title>Markdown@Edge</title>
<a href="/"><h1>Markdown@Edge</h1></a>
---

View File

@ -1,11 +1,12 @@
//! Default Compute@Edge template program.
//! Compute@Edge service which renders markdown from external sources.
use fastly::http::{header, Method, StatusCode};
use fastly::{mime, Body, Error, Request, Response};
use pulldown_cmark::{html, Options, Parser};
use std::io::Read;
/// The configured backend name.
/// The configured backend name. This is set up in the Fastly interface, and in fastly.toml
/// for local development.
const BACKEND_NAME: &str = "upstream_ssl";
/// The URL of the header markdown content.
@ -13,9 +14,12 @@ const HEADER_URL: &str = "http://nora.codes/edgeblog/_header.md";
/// The URL of the footer markdown content.
const FOOTER_URL: &str = "http://nora.codes/edgeblog/_footer.md";
/// This function is invoked for every request. If it returns Err(), the user will see a 500.
fn main() -> Result<(), Error> {
// Get the request sent by the client. This hooks into the Compute@Edge runtime interface.
let req = Request::from_client();
// Filter request methods...
// Filter out unrelated request methods.
match req.get_method() {
// Allow GET and HEAD requests.
&Method::GET | &Method::HEAD => (),
@ -30,33 +34,61 @@ fn main() -> Result<(), Error> {
}
};
// Rewrite the / path to index.md, and construct a Request
// Rewrite the / path to index.md, and construct a Request for the main body content.
let upstream_req = match req.get_path() {
"/" => Request::get("http://nora.codes/edgeblog/index.md"),
path => Request::get(format!("http://nora.codes/edgeblog/{}", path)),
};
// Get the data for the header portion of the page
// Get the data for the body portion of the page, and write the content into the Markdown
// document.
// This backend response is used as the template for the synthetic response.
let mut beresp = upstream_req.send(BACKEND_NAME)?;
// It's important to set Markdown's MIME type as "text" on your webserver;
// I used "text/markdown".
// Here, we check whether or not the object being requested is text. If so, we'll render it
// inline; otherwise, we'll serve it as is. This allows supporting images, other documents,
// and so forth without any extra effort.
if beresp
.get_content_type()
.map(|mime| mime.type_() != mime::TEXT)
.unwrap_or(true)
{
beresp.send_to_client();
return Ok(());
}
// Get the data for the header portion of the page, making a backend request to the origin
// server. This will be cached.
let mut header_body = match Request::get(HEADER_URL).send(BACKEND_NAME) {
Err(_e) => Body::new(),
Ok(mut r) => r.take_body(),
};
// Get the data for the footer portion of the page
// Start off the Markdown document with the header content.
let mut md_assembled = String::new();
header_body.read_to_string(&mut md_assembled)?;
md_assembled.push_str("\n");
beresp.take_body().read_to_string(&mut md_assembled)?;
// Get the data for the footer portion of the page, making a backend request to the origin
// server. This will be cached.
let mut footer_body = match Request::get(FOOTER_URL).send(BACKEND_NAME) {
Err(_e) => Body::new(),
Ok(mut r) => r.take_body(),
};
// Write the header into the synthetic body
let mut md_assembled = String::new();
header_body.read_to_string(&mut md_assembled)?;
// Write the footer into the synthetic body
md_assembled.push_str("\n");
footer_body.read_to_string(&mut md_assembled)?;
// Get the geoip info for the client, if possible.
const UNKNOWN_LOCATION_STRING: &str = "unknown";
let geotext: String = match req.get_client_ip_addr() {
Some(ip) => match fastly::geo::geo_lookup(ip) {
None => "an undisclosed location".into(),
None => UNKNOWN_LOCATION_STRING.into(),
Some(geo) => format!(
"AS{} '{}', in {}",
geo.as_number(),
@ -65,24 +97,11 @@ fn main() -> Result<(), Error> {
)
.into(),
},
None => "an undisclosed location".into(),
None => UNKNOWN_LOCATION_STRING.into(),
};
// Get the content of the page itself. If this fails, pass on the failure.
let mut beresp = upstream_req.send(BACKEND_NAME)?;
if beresp.get_status() == StatusCode::OK {
beresp.take_body().read_to_string(&mut md_assembled)?;
md_assembled = md_assembled.replace("{{geo}}", &geotext);
} else {
beresp.send_to_client();
return Ok(());
}
md_assembled.push_str(&format!("\n<h5> Your provider: {} </h5>", geotext));
// Write the footer into the synthetic body
md_assembled.push_str("\n");
footer_body.read_to_string(&mut md_assembled)?;
let md_assembled = md_assembled.replace("{{geo}}", &geotext);
let parser = Parser::new_ext(&md_assembled, Options::empty());
let mut client_body = beresp