Documentation

Domains, Sites & Reviews

Read and change player domains, Sitewright sites and Ratewell reviews from another resource.

Players buy domains on Namestead (namestead.com), build sites for them in Sitewright (sitewright.com) and review anything in the city on Ratewell (ratewell.com). The exports on this page let other resources hand out and move domains, draw a domain's site from their own page, read and moderate Sitewright sites, add live data blocks to them, and invite customers to leave a verified review.

These are player domains. A site your resource registers at a fixed address, such as a business page every player sees, is a different thing: see Apps & Sites.

General Information

Every export on this page is a server export. The config lives in shared/unlocked/domains.lua (Namestead under Config.Domains, Sitewright under Config.Domains.builder) and shared/unlocked/reviews.lua (Config.Reviews).

  • The domain exports only exist while Config.Domains.enabled is true.
  • The Sitewright exports only exist while both Config.Domains.enabled and Config.Domains.builder.enabled are true.
  • The review exports always exist. With Config.Reviews.enabled = false they answer nothing: CreateReviewInvite returns nil, "disabled", GetListingReviews an empty table and the others nil, so a resource needs no check of its own.

A resource that works without domains or Sitewright wraps its calls in pcall.

A host is matched in lower case, with any https:// and www. taken off, so "https://www.Bennys.com" finds bennys.com. A character is named by its framework identifier.

Providers and live data blocks belong to the resource that registered them and are removed when it stops. Exports marked Yields wait on the database. Call them from a thread, an event handler or a command, not while your script loads.

Types / Classes

DomainInfo, DomainProviderRegistration and DomainProvider are in types/domains.lua. SiteInfo, SiteStats, SiteBookingInfo, SiteEditorRecord, SiteBlockDefinition and SiteBlockInfo are in types/sites.lua. ReviewSummary and ReviewExport are in types/reviews.lua. All inside zyke_laptop.

#Domains

A domain is a DomainInfo:

FieldDescription
hostSuch as "bennys.com".
tldThe ending, such as "com".
identifierThe owner's character identifier.
ownerNameThe owner's character name.
mode"owned" or "rented".
state"active", or "expired" for a rental that ran out and is kept for its renter during graceDays.
expiresAtUnix seconds a rental runs until, nil when owned.
releaseAtUnix seconds an expired rental is released at, nil when owned.
providerId of the provider the domain is pointed at, nil for its parked page. Sitewright's is "sitewright".
title, description, categoryWhat the owner set in My domains, each optional.
listedShown in search results.
createdAtUnix seconds it was registered.

#Register Domain Provider

Adds a provider owners can point their domains at. A provider is a page in your resource that draws a domain's site, such as a blog. The page is the same for every domain: in it, LaptopSdk.ready() gives a context whose host is the domain it was opened on, so load that domain's content by host. See the JavaScript SDK.

Registering the same id again from the same resource replaces it. Returns false with a warning in the server console when id, label or ui is missing, the id isn't up to 64 letters, digits, _ and -, or another resource already registered it.

The laptop forgets providers when it restarts, so register again on the server event zyke_laptop:domains:Ready. Domains pointed at a provider that is gone show their parked page until it is back.

Example:

lua
local function register()
    ---@param provider DomainProviderRegistration
    ---@return boolean registered
    exports["zyke_laptop"]:RegisterDomainProvider({
        id = "blog",
        label = "Inkwell Blog",
        description = "A blog with posts and comments",
        icon = "pen-line", -- Lucide icon name or image URL
        ui = "web/index.html", -- A page in this resource, or a full URL
    })
end

CreateThread(register)
AddEventHandler("zyke_laptop:domains:Ready", register)
FieldDefaultDescription
idRequiredUnique provider id, up to 64 letters, digits, _ and -.
labelRequiredThe name owners pick in My domains.
descriptionNoneShown with the label.
iconNoneA Lucide icon name or an image URL.
uiRequiredA page in your resource, such as "web/index.html", or a full URL. List it under files in your fxmanifest.lua.

#Unregister Domain Provider

Removes a provider your resource registered. A provider of another resource is left alone.

Example:

lua
---@param providerId string
exports["zyke_laptop"]:UnregisterDomainProvider("blog")

#Get Domain Providers

Returns every registered provider as DomainProvider, sorted by label: {id, label, description?, icon?, ui, resource, builtin?}. ui is the page's full URL. Sitewright is listed as "sitewright" with builtin = true and an empty ui.

Example:

lua
---@return DomainProvider[] providers
local providers = exports["zyke_laptop"]:GetDomainProviders()

#Get Domain

Returns the domain as DomainInfo, or nil when nobody has registered it.

Example:

lua
---@param host string
---@return DomainInfo? domain
local domain = exports["zyke_laptop"]:GetDomain("bennys.com")

#Get Character Domains

Returns every domain the character holds, sorted by host, expired rentals included.

Example:

lua
---@param identifier string
---@return DomainInfo[] domains
local domains = exports["zyke_laptop"]:GetCharacterDomains(identifier)

#Get Provider Domains

Returns the active domains pointed at a provider, such as every blog your resource serves. Expired rentals are left out.

Example:

lua
---@param providerId string
---@return DomainInfo[] domains
local blogs = exports["zyke_laptop"]:GetProviderDomains("blog")

#Is Domain Owner

Returns whether the player's character owns the domain. false for an unknown domain and for a rental that ran out. Use it on the server before letting a player edit the content your provider shows.

Example:

lua
---@param plyId integer
---@param host string
---@return boolean owner
RegisterNetEvent("my_blog:SavePost", function(host, post)
    if (not exports["zyke_laptop"]:IsDomainOwner(source, host)) then return end

    -- Save the post for this host
end)

#Grant Domain

Gives a character a domain without payment. It also takes endings that aren't for sale and reserved names, such as lspd.gov for the police, and doesn't count towards the character's domain limit. The name before the ending must still fit Config.Domains.name (3 to 32 characters by default) of a-z, 0-9 and -.

mode is "owned" when left out. A "rented" domain needs days above 0 and runs out after that many days. Returns false, "invalid" for a bad identifier, host or mode, and false, "taken" when the domain is registered or being bought right now. Fires zyke_laptop:domains:Registered with no source and a price of 0. Yields until the domains are loaded.

Example:

lua
---@param identifier string
---@param host string
---@param mode? "owned" | "rented"
---@param days? integer @ How long a rental runs
---@return boolean granted
---@return string? err @ "invalid" or "taken"
local granted, err = exports["zyke_laptop"]:GrantDomain(chiefIdentifier, "lspd.gov")

#Release Domain

Deletes the domain, so anyone can register it again. Its Sitewright site, co-editors, bookings and stats are deleted with it, and its Ratewell listing is archived so the next owner starts clean. Fires zyke_laptop:domains:Released with the reason "script". Returns false for an unknown domain.

Example:

lua
---@param host string
---@return boolean released
local released = exports["zyke_laptop"]:ReleaseDomain("bennys.com")

#Set Domain Owner

Hands the domain to another character, such as after a sale your resource handled. A rental keeps its time left. The Sitewright site and Ratewell reviews go with it, the co-editors don't. Fires zyke_laptop:domains:OwnerChanged. Returns false for an unknown domain or an identifier that isn't a string.

Players can also hand domains to each other from Namestead, see the README of zyke_laptop.

Example:

lua
---@param host string
---@param identifier string
---@return boolean changed
exports["zyke_laptop"]:SetDomainOwner("bennys.com", buyerIdentifier)

#Set Domain Provider

Points the domain at a provider, or at its parked page with false. Fires zyke_laptop:domains:ProviderChanged when it changed. Returns false, "notFound" for an unknown domain and false, "providerMissing" for a provider that isn't registered.

Example:

lua
---@param host string
---@param providerId string | false
---@return boolean changed
---@return string? err @ "notFound" or "providerMissing"
local changed, err = exports["zyke_laptop"]:SetDomainProvider("alexmoreno.blog", "blog")

#Domain Events

Server events, with the domain as DomainInfo. Listeners get a copy, never the stored domain.

EventArguments
zyke_laptop:domains:Registeredsource, domain, price. source is nil for GrantDomain.
zyke_laptop:domains:Renewedsource, domain, price
zyke_laptop:domains:BoughtOutsource, domain, price, a rental bought to keep.
zyke_laptop:domains:Expireddomain
zyke_laptop:domains:Releaseddomain, reason, where reason is "owner", "expired" or "script".
zyke_laptop:domains:OwnerChangeddomain, previousOwner
zyke_laptop:domains:ProviderChangeddomain, previousProvider
zyke_laptop:domains:ReadyFires once the domains are loaded. Register your providers again here.

#Sitewright

Sitewright is the built-in site builder. Its exports read a domain's site, take it down and name its co-editors. A site is a SiteInfo:

FieldDescription
hostThe domain.
ownerThe domain owner's character identifier.
liveVisitors of the domain see the published site.
versionCounted up by every publish, unpublish and delete.
hasDraftThe site has a saved draft.
updatedAt, publishedAtUnix seconds, each optional.
takedown{reason, locked, by, at} while staff or a resource took it down. by is the staff character's identifier, "server" for the console, or the resource's name.
docThe published site, only from GetSite while it is live. See nui_source/src/sites/doc/types.ts for its fields.
statsOnly from GetSite while stats are on: {week, month}, the totals of the last 7 and 30 days, each {views, visitors, forms, bookings}.

#Get Site

Returns the site on the domain, or nil for a domain without a Sitewright site (no draft, nothing published and no takedown) or while sites are still loading. Yields while it reads the published site and stats from the database.

Example:

lua
---@param host string
---@return SiteInfo? site
local site = exports["zyke_laptop"]:GetSite("bennys.com")

if (site and site.stats) then print(("%s had %d views this week"):format(site.host, site.stats.week.views)) end

#Get Site Stats

Returns the site's visitor stats for the last days days, today included. days is 30 when left out and at most builder.stats.retentionDays. Returns nil for a domain without a Sitewright site and while builder.stats.enabled is false. Yields.

The result is {host, days, views, visitors, forms, bookings, daily, entries}. visitors adds up each day's unique visitors. daily lists {day, views, visitors, forms, bookings} oldest first, with day as YYYY-MM-DD in server time. entries lists the pages and posts most viewed first as {kind, id, title?, views}, where kind is "page" or "post" and title is nil once it is gone from the site.

The owner's and co-editors' own visits, messages and bookings never count.

Example:

lua
---@param host string
---@param days? integer
---@return SiteStats? stats
local stats = exports["zyke_laptop"]:GetSiteStats("bennys.com", 7)

#Get Site Bookings

Returns every booking the site's booking blocks took that is still kept, earliest first: upcoming, past, done and cancelled ones. Empty for a domain without bookings and while builder.bookings.enabled is false. Yields.

Each booking is {id, host, owner, block, service, serviceName, minutes, startsAt, endsAt, identifier, name, note, from, to, status, cancelledBy?, createdAt}. Times are unix seconds. identifier is the visitor's character, name the name they gave, from their Nimbus address and to the site's address the booking was mailed to. status is "booked", "cancelled" or "done", and cancelledBy is "visitor" or "site".

Example:

lua
---@param host string
---@return SiteBookingInfo[] bookings
local bookings = exports["zyke_laptop"]:GetSiteBookings("bennys.com")

#Is Site Published

Returns whether visitors of the domain see its Sitewright site.

Example:

lua
---@param host string
---@return boolean live
local live = exports["zyke_laptop"]:IsSitePublished("bennys.com")

#Take Down Site

Does the same as /laptop_site_takedown, or /laptop_site_lock with lock = true, with your resource as who took it down. It unpublishes the site, points a domain that was pointed at Sitewright back at its parked page, and tells the owner the reason in Sitewright, as a notification while they are online, and by mail while builder.moderation.mailOwner is on and Mail runs. The reason is cut to 200 characters.

Without lock, the owner may publish again once it is fixed. With lock, every publish is refused until LiftSiteTakedown, and it also works on a site that was never published.

Returns false, "notFound" for an unknown domain and false, "notLive" for a takedown without lock of a site that isn't published. Fires zyke_laptop:sites:Unpublished with the cause "takedown" when it was live, then zyke_laptop:sites:TakenDown.

Example:

lua
---@param host string
---@param reason? string @ Shown to the owner
---@param lock? boolean @ Refuse every publish until LiftSiteTakedown
---@return boolean takenDown
---@return string? err @ "notFound" or "notLive"
local done, err = exports["zyke_laptop"]:TakeDownSite("bennys.com", "Impersonates a real business", true)

#Lift Site Takedown

Does the same as /laptop_site_unlock. The owner may publish again, and the site stays offline until they do. Returns false, "notFound" for an unknown domain or one without a site, and false, "noTakedown" when it isn't taken down. Fires zyke_laptop:sites:TakedownLifted.

Example:

lua
---@param host string
---@return boolean lifted
---@return string? err @ "notFound" or "noTakedown"
local lifted, err = exports["zyke_laptop"]:LiftSiteTakedown("bennys.com")

#Get Site Editors

Returns the site's co-editors as {identifier, role, addedAt}, the first one named first. role is "editor", who edits and saves the draft, or "publisher", who can also publish and take it offline. The domain's owner isn't one of them.

Example:

lua
---@param host string
---@return SiteEditorRecord[] editors
local editors = exports["zyke_laptop"]:GetSiteEditors("bennys.com")

#Set Site Editor

Gives a character the "editor" or "publisher" role on the site, or takes their access away with false, such as for the members of a job. The same as the owner doing it in Sitewright: the character is notified while online, and an editor they have open closes when they lose access. Fires zyke_laptop:sites:EditorChanged with no source. Setting a role the character already has returns true and changes nothing. Yields.

Adding someone needs an active domain and room under builder.editors.max (8 by default). Taking a role away always works.

ErrorWhen
notFoundThe domain doesn't exist, is past its grace days, or sites are still loading.
invalididentifier isn't a string, or role isn't "editor", "publisher" or false.
disabledbuilder.editors.enabled is false.
isOwnerThe character owns the domain.
expiredThe rental ran out, so no one new can be added.
editorLimitThe site already has builder.editors.max co-editors.

Example:

lua
---@param host string
---@param identifier string
---@param role "editor" | "publisher" | false
---@return boolean done
---@return string? err
local done, err = exports["zyke_laptop"]:SetSiteEditor("bennys.com", mechanicIdentifier, "editor")

#Live Data Blocks

A live data block shows what the server knows right now on a player's site, such as a dealership's stock, the houses for sale, whether a business is open or a casino's jackpot. Owners find the blocks in the editor's Add panel under Live data, with the settings your resource asks for in the inspector. The laptop ships three: hireline (open Hireline listings), duty (on duty badge and count) and ratewell (a Ratewell listing's score and latest reviews).

When someone views the page, the server asks your resource for the data, with the block and settings read from the published site, never from the visitor. The answer is kept for builder.data.cacheSeconds (30 by default) per site, block and settings, and an open page asks again every minute. A data function that errors or takes longer than builder.data.timeoutMs (2500 by default) shows a quiet error in the block, and a block whose resource stopped shows as unavailable until it is registered again. A page or post holds up to builder.data.perPage live data blocks, and builder.data.enabled = false turns them off.

examples/siteblock/ inside zyke_laptop is a complete dealership block to copy. Turn it on with siteBlock.enabled in shared/unlocked/examples.lua.

#Register Site Block

Adds a live data block owners can put on their sites. Registering the same id again from the same resource replaces it and drops its kept answers. Returns false, "invalid" for a bad id or definition and false, "taken" when another resource already registered the id, with the reason in the server console.

data(settings, context) returns the data in the shape of the block's layout, or nil for an error. It may yield, such as for a database query. settings holds every setting you declared, already read against its type and limits. context is {host, owner, viewer, preview}: owner is the domain owner's identifier, viewer is {source, identifier, name} and preview is true when the editor asks. Unless perViewer is set, one answer is shown to every visitor for the cache time, so viewer is only who triggered the fetch.

A block goes away when your resource stops and when the laptop restarts, so register it again on the server event zyke_laptop:Ready.

Example:

lua
local function register()
    ---@param blockId string
    ---@param definition SiteBlockDefinition
    ---@return boolean registered
    ---@return string? err @ "invalid" or "taken"
    exports["zyke_laptop"]:RegisterSiteBlock("my_dealership", {
        label = "Dealership stock",
        description = "The cars on sale with their price.",
        icon = "car", -- A Lucide icon name
        layout = "cards",
        settings = {
            {key = "category", type = "select", label = "Category", options = function(context) return getCategories() end},
            {key = "count", type = "number", label = "Cars shown", min = 1, max = 12, default = 6},
            {key = "soldOut", type = "toggle", label = "Show sold-out cars", default = false},
        },
        data = function(settings, context)
            return {cards = getCars(settings.category, settings.count, settings.soldOut), empty = "Nothing in stock right now."}
        end,
        sample = {cards = {{title = "Karin Sultan", price = "$35,000"}}},
    })
end

register()
AddEventHandler("zyke_laptop:Ready", register)
FieldDefaultDescription
labelRequiredUp to 60 characters.
descriptionNoneUp to 200 characters.
icon"activity"A Lucide icon name of a-z, 0-9 and -.
layoutRequired"stat", "list", "cards" or "table". See Layouts.
settingsNoneUp to 12 settings the owner picks in the inspector.
dataRequiredfunction(settings, context), returns the data or nil.
sampleNoneData in the layout's shape, shown in the editor while data fails.
cacheSecondsbuilder.data.cacheSecondsSeconds to keep an answer, 0 to 3600. 0 keeps nothing.
perViewerfalsetrue keeps an answer per viewer instead of one for every visitor.

The block id is up to 64 letters, digits, _ and -.

#Settings

FieldDescription
keyUp to 32 letters, digits and _, starting with a letter. Each key once per block.
type"text", "number", "select" or "toggle".
labelUp to 60 characters, the key when left out.
hintOptional, up to 160 characters.
defaultText: empty. Number: min. Toggle: false. Select: the first option.
min, maxnumber only, whole numbers.
maxLengthtext only, 100 when left out, at most 200.
optionsselect only. A list of {value, label} or plain values, up to 100, or a function(context) that returns one, asked each time an owner opens the block. A select with a list needs at least one option.

#Layouts

Your resource never sends markup. The data has to fit one of four layouts, drawn in the site's own fonts and colours. Every text is cut to its limit, pictures must come from builder.imageHosts, links must be web addresses or gps:x,y, and anything else is left out.

LayoutShape
stat{badge?, stats = {{label, value}, ...}}, up to 4 stats, for an open or closed badge and big numbers.
list{rows = {{label, value, icon?}, ...}}, up to 20 rows.
cards{cards = {{title, subtitle?, image?, price?, badge?, link?}, ...}}, up to 24 cards, such as listings or cars.
table{columns = {"Name", ...}, rows = {{"cell", 12, ...}, ...}}, up to 6 columns and 30 rows.

Every layout also takes note, a line under the block, and empty, shown instead of an empty list. A badge is {label, tone} with a tone of "neutral", "good", "warn" or "bad", or just its label. Values may be text or numbers.

#Unregister Site Block

Removes a block your resource registered. A block of another resource is left alone. Sites using it keep it with its settings and show it as unavailable until it is registered again.

Example:

lua
---@param blockId string
exports["zyke_laptop"]:UnregisterSiteBlock("my_dealership")

#Refresh Site Block

Drops the block's kept answers, so the next view gets fresh data instead of waiting for the cache time, such as after the stock it lists changed.

Example:

lua
---@param blockId string
exports["zyke_laptop"]:RefreshSiteBlock("my_dealership")

#Get Site Blocks

Returns every registered block, sorted by label, as {id, label, description?, icon, layout, settings, resource}, without its functions. A select whose options come from a function has dynamic = true and no options.

Example:

lua
---@return SiteBlockInfo[] blocks
local blocks = exports["zyke_laptop"]:GetSiteBlocks()

#Site Events

Server events, with the site as SiteInfo. actor is who did it in Sitewright, {identifier, name, role} with role as "owner", "publisher" or "editor".

EventArguments
zyke_laptop:sites:Publishedsource, site, actor
zyke_laptop:sites:Unpublishedsource?, site, cause, actor?. cause is "unpublished", "deleted" or "takedown". A takedown has the staff player or nil as source and no actor.
zyke_laptop:sites:Deletedsource, site, actor
zyke_laptop:sites:EditorChangedsource?, site, {identifier, role?, previous?}. source is nil for SetSiteEditor, role is nil once their access was taken away.
zyke_laptop:sites:TakenDownstaffId?, site, takedown
zyke_laptop:sites:TakedownLiftedstaffId?, site
zyke_laptop:sites:FormSubmittedsource, {host, owner, form, title, to, from, answers, messageKey}. answers holds name, phone, subject and message.
zyke_laptop:sites:BookingCreatedsource, booking, as GetSiteBookings lists it.
zyke_laptop:sites:BookingCancelledsource?, booking, by, where by is "visitor" or "site".
zyke_laptop:sites:DayStatshost, {day, views, visitors, forms, bookings}. Fires once a day after midnight for each site that had visits, messages or bookings the day before, such as to pay out for traffic. A day the server wasn't running over midnight isn't reported.

A site also stops showing when its domain is pointed at another provider, runs out or is released, which the domain events report.

#Reviews

Ratewell listings are named by their slug, such as "autorepair", or by {kind, ref}:

Kindref
businessThe MDT department id, such as "autorepair".
jobThe framework job name, for jobs in listings.jobs.
siteThe domain, such as "bennys.com". An active domain's listing is made the first time it is asked for.
placeThe place id from listings.places.

Only open listings are found. A renamed or merged listing is followed to where it lives now.

#Create Review Invite

Invites a customer to review a listing, such as after your resource did a job for them. The invite is mailed with a link from invites.sender, works only for that character, and a review written from it is Verified and counts score.verifiedWeight times. Yields.

ref is unique per resource, so a retry with the same ref returns the first invite's token and sends nothing new. Another invite for the same customer and listing refreshes their open one, and is mailed again only after invites.mailCooldownHours. Fires zyke_laptop:reviews:InviteCreated.

Returns the invite's token, or nil and "invalid", "notFound" (no open listing), "member" (the customer works there), "own" (the customer manages the listing or owns the domain) or "disabled" (Ratewell is off).

Example:

lua
---@param target integer | string @ Server id or character identifier of the customer
---@param data {listing: string | {kind: string, ref: string}, service?: string | integer, handlers?: (integer | string)[], ref: string, days?: integer, mail?: boolean}
---@return string? token
---@return string? err
local token, err = exports["zyke_laptop"]:CreateReviewInvite(customerId, {
    listing = {kind = "business", ref = "autorepair"},
    service = "Roadside tow",
    handlers = {source},
    ref = ("tow:%d"):format(jobId),
})
FieldDefaultDescription
listingRequiredA slug or {kind, ref}.
refRequiredYour own id for this job, up to 96 characters, unique per resource.
serviceNoneA service id of the listing, or a label. A label the listing doesn't have yet is added as your resource's service.
handlersNoneServer ids or identifiers of the staff who did it, up to 8. Credited on a verified review. The customer is left out.
daysinvites.daysDays the invite works, 1 to 60.
mailtruefalse creates the invite without mailing it.

#Register Review Service

Offers a service on a listing to reviewers, such as "Roadside tow", kept across restarts. Each service has its own score. Registering the same id again from the same resource returns the same service and updates its label. Returns the service id, or nil for a listing that isn't found, an id that is empty or longer than 64 characters, or an empty label. Yields.

This is a server export only. There is no client side.

Example:

lua
---@param listing string | {kind: string, ref: string}
---@param service {id: string, label: string} @ label up to 64 characters
---@return integer? serviceId
local serviceId = exports["zyke_laptop"]:RegisterReviewService("autorepair", {id = "tow", label = "Roadside tow"})

#Get Review Summary

Returns the listing's summary, or nil when it isn't found or Ratewell is still loading: {slug, url, name, score?, count, verifiedCount, stars, services}. score is the Ratewell score, nil until the listing has score.minReviews reviews. stars counts the reviews per star. services lists {id, label, score?, count}. Yields.

Example:

lua
---@param listing string | {kind: string, ref: string}
---@return ReviewSummary? summary
local summary = exports["zyke_laptop"]:GetReviewSummary({kind = "site", ref = "bennys.com"})

#Get Listing Reviews

Returns the listing's latest visible reviews, newest first, as {id, rating, title, body, authorName, verified, service?, createdAt, url}. count is 5 when left out and at most 50. serviceId keeps only one service's reviews and verifiedOnly only verified ones. Empty when the listing isn't found or Ratewell is off or still loading. Yields.

Example:

lua
---@param listing string | {kind: string, ref: string}
---@param options? {count?: integer, serviceId?: integer, verifiedOnly?: boolean}
---@return ReviewExport[] reviews
local reviews = exports["zyke_laptop"]:GetListingReviews("autorepair", {count = 3, verifiedOnly = true})

#Get Review Listing Url

Returns the listing's address, such as https://ratewell.com/autorepair, or nil when it isn't found. Yields.

Example:

lua
---@param listing string | {kind: string, ref: string}
---@return string? url
local url = exports["zyke_laptop"]:GetReviewListingUrl("autorepair")

#Review Events

Server events, with the listing as GetReviewSummary gives it and a review as GetListingReviews lists it.

EventArguments
zyke_laptop:reviews:ReviewPostedlisting, review, identifier
zyke_laptop:reviews:ReviewEditedlisting, review, identifier
zyke_laptop:reviews:ReplyPostedlisting, reviewId
zyke_laptop:reviews:ReviewRemovedlisting, reviewId, reason
zyke_laptop:reviews:InviteCreatedlisting, identifier, token