How to implement dynamic SEO in react?

Hello everyone!
I am working on react app project that hosted on internet computer.
Can I implement dynamic SEO on that?
For example, when we share user profile link on the social platform, we should show user’s detail and photo as preview.
In general, we should use SSR(Server Side Rendering) to implement this.
Is there any features to support SEO on the internet computer?

2 Likes

SEO is supported, but setting dynamic meta tags for social media previews can be challenging, especially for single page applications built with React. From my experience, if you’re using React, only the meta tags you define in your index.html (your entry point) will be the ones shown for social media previews. SSR is not supported on the IC. You may opt to prerender your pages (which might be easier if you’re using Nextjs), but prerendering user profiles is not practical.

1 Like

Thank you for your reply.
You mean we can’t do it on the IC?

It’s possible, but it needs quite a bit of ICP-specific logic that nobody has written yet. Have a look at motoko server. It’s a first version of SSR in Motoko

1 Like

Generating dynamic metatags, ogimages etc is not too difficult to setup for a SPA on the Internet Computer. There are no libraries etc to support you as far as I know though.

See this demo I did a while back as an example:

Live demo: https://2lv2n-giaaa-aaaal-qjuda-cai.icp0.io/

2 Likes

Thank you. This is good solution.
btw, I am not using canister as a backend.
Backend is hosted on the AWS, and frontend is hosted on IC
Anyone have solution of this?

Ok, that is trickier then. The canister that serves the frontend needs to have knowledge about what should go into the meta tags etc. One option would be to let the AWS backend push some of that data to the frontend canister when the data changes. Another option would be to let the frontend canister query AWS first time a page is generated. If metadata for a page don’t change after creation, that could be an ok option. If you would have to make that request every time a page loads then it sounds like a slow and expensive setup.

Similar considerations would have to be made for a plain web2 architecture where the frontend is served from a location that does not directly manage the backend data. One difference of course is that in a plain web2 architecture, making a call to the AWS backend is fast and cheap, while on ICP it is comparatively slow and expensive.

I’ve run into this in a few projects now, do you think it could ever be feasible for dfinity to launch a standard frontend canister that allows for custom dynamic SEO/social sharing?

If I understand correctly the options are to a. build a custom frontend canister b. use centralized server to route/hydrate requests

I’m considering biting the bullet and forking the assets canister, it can’t be that bad… :sweat_smile:

1 Like

Happy to give you some pointers if you want to fork the asset canister. It’s not that bad but it takes time to debug and design a nice interface. I’d love to put more time in but there’s always ‘more important’ stuff to do

If you would like to make a PR instead I’d love to help you with that too. Just ping me :slightly_smiling_face:

1 Like

Totally understand, it would be cool to contribute a PR, I will definitely take you up on a review when we get to that stage. I’ve started investigating the code to get an MVP together, thanks @Severin

1 Like

The example I shared above, essentially is a simplified asset canister.

  1. Accept an incoming query request for an ogimage
  2. If a certified asset (the image) exists, serve it
  3. If a certified asset don’t exist, upgrade the call to an update call
  4. In the update call, generate the asset, certify it and serve it

The example focuses on ogimages. But, the asset served could just as well be the main index.html for a frontend application, with custom metadata set, based on the request.

1 Like

Thanks, exactly I read through your example, very helpful demo. I use a lot of the functions of the standard asset canister otherwise I would simplify more rather than forking. If I understand correctly I can route requests by checking the user agent. In the case of user agents seeking social previews, I can serve the HTML social preview, in the case of users, they are directed as normal.

1 Like

Yup, I guess you can check the user agent.

In another project I serve the full frontend this way and hydrate the index.html of specific URLs when requested.

When initialising the canister, I certify all prebuilt frontend assets. asset_util comes from the Dfinity Internet Identity repository weirdly enough.

asset_util: internet-identity/src/asset_util at main · dfinity/internet-identity · GitHub

use asset_util::{collect_assets, Asset, CertifiedAssets, ContentEncoding, ContentType};

pub static ASSET_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../catts_frontend/dist");

pub fn init_assets() {
    let assets = collect_assets(&ASSET_DIR, None);
    ASSETS.with_borrow_mut(|certified_assets| {
        *certified_assets = CertifiedAssets::certify_assets(assets, &default_headers());
    });
    update_root_hash();
}

Then, when a request comes in, I serve those assets or upgrade the request to an update call, if url starts with “/recipe”, “/run” or “/user”. Here you could check user agent instead.

    let upgrade_requests = ["/recipe", "/run", "/user"];
    if upgrade_requests.iter().any(|&url| req_url.starts_with(url)) {
        return HttpResponse {
            status_code: 404,
            headers: default_headers(),
            body: vec![],
            upgrade: Some(true),
        };
    }

The function that accepts the update call looks like this:

use crate::{
    certified_data::{render_recipe_assets, render_run_assets, render_user_assets},
    http_request::http::{default_headers, http_error, HttpRequest, HttpResponse},
};

#[ic_cdk::update]
async fn http_request_update(req: HttpRequest) -> HttpResponse {
    let path_segments: Vec<&str> = req.url.split('/').filter(|s| !s.is_empty()).collect();
    let render_result = match path_segments.as_slice() {
        ["user", user_id] => render_user_assets(user_id.to_string()),
        ["recipe", recipe_name] => render_recipe_assets(recipe_name.to_string()),
        ["run", run_id] => render_run_assets(run_id.to_string()),
        _ => {
            return http_error(404, "Not found.");
        }
    };

    let assets = match render_result {
        Ok(assets) => assets,
        Err(err) => {
            return http_error(500, &format!("{}", err));
        }
    };

    HttpResponse {
        status_code: 200,
        body: assets[0].clone().content,
        headers: default_headers(),
        upgrade: None,
    }
}

And one of the render_x functions like this. It creates and index.html asset with the right metadata as well as an ogimage from a template svg.

pub fn render_user_assets(address: String) -> Result<Vec<Asset>, AssetError> {
    let index_asset = render_index_html(
        format!("/user/{}", address),
        json!({
            "ogimage": format!("/user/{}/ogimage.png", address),
            "title": "C-ATTS, Composite Attestations",
            "description": "Move, transform and combine attestations! C-ATTS introduces the concept of composite attestations, a new type of attestation combining data from multiple sources to form a unified and verifiable credential.",
        }),
    );

    let eth_address = EthAddress::new(&address).map_err(|_| AssetError::InvalidArgument)?;
    let user = user::get_by_eth_address(&eth_address).map_err(|_| AssetError::NotFound)?;

    let ogimage_asset = render_asset(
        include_str!("includes/ogimage_template_user.svg"),
        json!({"eth_address": user.eth_address}),
        &format!("user/{}", address),
    );

    certify_and_update_assets(vec![index_asset.clone(), ogimage_asset.clone()]);

    Ok(vec![index_asset, ogimage_asset])
}

Full repo here:

1 Like